Index: ps/trunk/source/scriptinterface/ScriptRuntime.h =================================================================== --- ps/trunk/source/scriptinterface/ScriptRuntime.h (revision 24180) +++ ps/trunk/source/scriptinterface/ScriptRuntime.h (nonexistent) @@ -1,102 +0,0 @@ -/* Copyright (C) 2020 Wildfire Games. - * This file is part of 0 A.D. - * - * 0 A.D. is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * 0 A.D. is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with 0 A.D. If not, see . - */ - -#ifndef INCLUDED_SCRIPTRUNTIME -#define INCLUDED_SCRIPTRUNTIME - -#include "ScriptTypes.h" -#include "ScriptExtraHeaders.h" - -#include - -constexpr int STACK_CHUNK_SIZE = 8192; - -// Those are minimal defaults. The runtime for the main game is larger and GCs upon a larger growth. -constexpr int DEFAULT_RUNTIME_SIZE = 16 * 1024 * 1024; -constexpr int DEFAULT_HEAP_GROWTH_BYTES_GCTRIGGER = 2 * 1024 * 1024; - -/** - * Abstraction around a SpiderMonkey JSRuntime/JSContext. - * - * A single ScriptRuntime, with the associated runtime and context, - * should only be used on a single thread. - * - * (One means to share data between threads and runtimes is to create - * a ScriptInterface::StructuredClone.) - */ - -class ScriptRuntime -{ -public: - ScriptRuntime(int runtimeSize, int heapGrowthBytesGCTrigger); - ~ScriptRuntime(); - - /** - * Returns a runtime/context, in which any number of ScriptInterfaces compartments can live. - * Each runtime should only ever be used on a single thread. - * @param runtimeSize Maximum size in bytes of the new runtime - * @param heapGrowthBytesGCTrigger Size in bytes of cumulated allocations after which a GC will be triggered - */ - static shared_ptr CreateRuntime( - int runtimeSize = DEFAULT_RUNTIME_SIZE, - int heapGrowthBytesGCTrigger = DEFAULT_HEAP_GROWTH_BYTES_GCTRIGGER); - - /** - * MaybeIncrementalRuntimeGC tries to determine whether a runtime-wide garbage collection would free up enough memory to - * be worth the amount of time it would take. It does this with our own logic and NOT some predefined JSAPI logic because - * such functionality currently isn't available out of the box. - * It does incremental GC which means it will collect one slice each time it's called until the garbage collection is done. - * This can and should be called quite regularly. The delay parameter allows you to specify a minimum time since the last GC - * in seconds (the delay should be a fraction of a second in most cases though). - * It will only start a new incremental GC or another GC slice if this time is exceeded. The user of this function is - * responsible for ensuring that GC can run with a small enough delay to get done with the work. - */ - void MaybeIncrementalGC(double delay); - void ShrinkingGC(); - - /** - * This is used to keep track of compartments which should be prepared for a GC. - */ - void RegisterCompartment(JSCompartment* cmpt); - void UnRegisterCompartment(JSCompartment* cmpt); - - JSRuntime* GetJSRuntime() const { return m_rt; } - - /** - * GetGeneralJSContext returns the context without starting a GC request and without - * entering any compartment. It should only be used in specific situations, such as - * creating a new compartment, or as an unsafe alternative to GetJSRuntime. - * If you need the compartmented context of a ScriptInterface, you should create a - * ScriptInterface::Request and use the context from that. - */ - JSContext* GetGeneralJSContext() const { return m_cx; } - -private: - - JSRuntime* m_rt; - JSContext* m_cx; - - void PrepareCompartmentsForIncrementalGC() const; - std::list m_Compartments; - - int m_RuntimeSize; - int m_HeapGrowthBytesGCTrigger; - int m_LastGCBytes; - double m_LastGCCheck; -}; - -#endif // INCLUDED_SCRIPTRUNTIME Property changes on: ps/trunk/source/scriptinterface/ScriptRuntime.h ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Index: ps/trunk/source/scriptinterface/ScriptRuntime.cpp =================================================================== --- ps/trunk/source/scriptinterface/ScriptRuntime.cpp (revision 24180) +++ ps/trunk/source/scriptinterface/ScriptRuntime.cpp (nonexistent) @@ -1,324 +0,0 @@ -/* Copyright (C) 2020 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 "ScriptRuntime.h" - -#include "ps/GameSetup/Config.h" -#include "ps/Profile.h" -#include "scriptinterface/ScriptEngine.h" -#include "scriptinterface/ScriptInterface.h" - - -void GCSliceCallbackHook(JSRuntime* UNUSED(rt), JS::GCProgress progress, const JS::GCDescription& UNUSED(desc)) -{ - /* - * During non-incremental GC, the GC is bracketed by JSGC_CYCLE_BEGIN/END - * callbacks. During an incremental GC, the sequence of callbacks is as - * follows: - * JSGC_CYCLE_BEGIN, JSGC_SLICE_END (first slice) - * JSGC_SLICE_BEGIN, JSGC_SLICE_END (second slice) - * ... - * JSGC_SLICE_BEGIN, JSGC_CYCLE_END (last slice) - */ - - - if (progress == JS::GC_SLICE_BEGIN) - { - if (CProfileManager::IsInitialised() && ThreadUtil::IsMainThread()) - g_Profiler.Start("GCSlice"); - g_Profiler2.RecordRegionEnter("GCSlice"); - } - else if (progress == JS::GC_SLICE_END) - { - if (CProfileManager::IsInitialised() && ThreadUtil::IsMainThread()) - g_Profiler.Stop(); - g_Profiler2.RecordRegionLeave(); - } - else if (progress == JS::GC_CYCLE_BEGIN) - { - if (CProfileManager::IsInitialised() && ThreadUtil::IsMainThread()) - g_Profiler.Start("GCSlice"); - g_Profiler2.RecordRegionEnter("GCSlice"); - } - else if (progress == JS::GC_CYCLE_END) - { - if (CProfileManager::IsInitialised() && ThreadUtil::IsMainThread()) - g_Profiler.Stop(); - g_Profiler2.RecordRegionLeave(); - } - - // The following code can be used to print some information aobut garbage collection - // Search for "Nonincremental reason" if there are problems running GC incrementally. - #if 0 - if (progress == JS::GCProgress::GC_CYCLE_BEGIN) - printf("starting cycle ===========================================\n"); - - const char16_t* str = desc.formatMessage(rt); - int len = 0; - - for(int i = 0; i < 10000; i++) - { - len++; - if(!str[i]) - break; - } - - wchar_t outstring[len]; - - for(int i = 0; i < len; i++) - { - outstring[i] = (wchar_t)str[i]; - } - - printf("---------------------------------------\n: %ls \n---------------------------------------\n", outstring); - #endif -} - - -namespace { - -void ErrorReporter(JSContext* cx, const char* message, JSErrorReport* report) -{ - ScriptInterface::Request rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); - - std::stringstream msg; - bool isWarning = JSREPORT_IS_WARNING(report->flags); - msg << (isWarning ? "JavaScript warning: " : "JavaScript error: "); - if (report->filename) - { - msg << report->filename; - msg << " line " << report->lineno << "\n"; - } - - msg << message; - - // If there is an exception, then print its stack trace - JS::RootedValue excn(rq.cx); - if (JS_GetPendingException(rq.cx, &excn) && excn.isObject()) - { - JS::RootedValue stackVal(rq.cx); - JS::RootedObject excnObj(rq.cx, &excn.toObject()); - JS_GetProperty(rq.cx, excnObj, "stack", &stackVal); - - std::string stackText; - ScriptInterface::FromJSVal(rq, stackVal, stackText); - - std::istringstream stream(stackText); - for (std::string line; std::getline(stream, line);) - msg << "\n " << line; - } - - if (isWarning) - LOGWARNING("%s", msg.str().c_str()); - else - LOGERROR("%s", msg.str().c_str()); - - // When running under Valgrind, print more information in the error message - // VALGRIND_PRINTF_BACKTRACE("->"); -} - -} // anonymous namespace - - -shared_ptr ScriptRuntime::CreateRuntime(int runtimeSize, int heapGrowthBytesGCTrigger) -{ - return shared_ptr(new ScriptRuntime(runtimeSize, heapGrowthBytesGCTrigger)); -} - -ScriptRuntime::ScriptRuntime(int runtimeSize, int heapGrowthBytesGCTrigger): - m_LastGCBytes(0), - m_LastGCCheck(0.0f), - m_HeapGrowthBytesGCTrigger(heapGrowthBytesGCTrigger), - m_RuntimeSize(runtimeSize) -{ - ENSURE(ScriptEngine::IsInitialised() && "The ScriptEngine must be initialized before constructing any ScriptRuntimes!"); - - m_rt = JS_NewRuntime(runtimeSize, JS::DefaultNurseryBytes, nullptr); - ENSURE(m_rt); // TODO: error handling - - JS::SetGCSliceCallback(m_rt, GCSliceCallbackHook); - - JS_SetGCParameter(m_rt, JSGC_MAX_MALLOC_BYTES, m_RuntimeSize); - JS_SetGCParameter(m_rt, JSGC_MAX_BYTES, m_RuntimeSize); - JS_SetGCParameter(m_rt, JSGC_MODE, JSGC_MODE_INCREMENTAL); - - // The whole heap-growth mechanism seems to work only for non-incremental GCs. - // We disable it to make it more clear if full GCs happen triggered by this JSAPI internal mechanism. - JS_SetGCParameter(m_rt, JSGC_DYNAMIC_HEAP_GROWTH, false); - - ScriptEngine::GetSingleton().RegisterRuntime(m_rt); - - - m_cx = JS_NewContext(m_rt, STACK_CHUNK_SIZE); - ENSURE(m_cx); // TODO: error handling - - JS_SetOffthreadIonCompilationEnabled(m_rt, true); - - // For GC debugging: - // JS_SetGCZeal(m_cx, 2, JS_DEFAULT_ZEAL_FREQ); - - JS_SetContextPrivate(m_cx, nullptr); - - JS_SetErrorReporter(m_rt, ErrorReporter); - - JS_SetGlobalJitCompilerOption(m_rt, JSJITCOMPILER_ION_ENABLE, 1); - JS_SetGlobalJitCompilerOption(m_rt, JSJITCOMPILER_BASELINE_ENABLE, 1); - - JS::RuntimeOptionsRef(m_cx) - .setExtraWarnings(true) - .setWerror(false) - .setStrictMode(true); -} - -ScriptRuntime::~ScriptRuntime() -{ - JS_DestroyContext(m_cx); - JS_DestroyRuntime(m_rt); - - ENSURE(ScriptEngine::IsInitialised() && "The ScriptEngine must be active (initialized and not yet shut down) when destroying a ScriptRuntime!"); - ScriptEngine::GetSingleton().UnRegisterRuntime(m_rt); -} - -void ScriptRuntime::RegisterCompartment(JSCompartment* cmpt) -{ - ENSURE(cmpt); - m_Compartments.push_back(cmpt); -} - -void ScriptRuntime::UnRegisterCompartment(JSCompartment* cmpt) -{ - m_Compartments.remove(cmpt); -} - -#define GC_DEBUG_PRINT 0 -void ScriptRuntime::MaybeIncrementalGC(double delay) -{ - PROFILE2("MaybeIncrementalGC"); - - if (JS::IsIncrementalGCEnabled(m_rt)) - { - // The idea is to get the heap size after a completed GC and trigger the next GC when the heap size has - // reached m_LastGCBytes + X. - // In practice it doesn't quite work like that. When the incremental marking is completed, the sweeping kicks in. - // The sweeping actually frees memory and it does this in a background thread (if JS_USE_HELPER_THREADS is set). - // While the sweeping is happening we already run scripts again and produce new garbage. - - const int GCSliceTimeBudget = 30; // Milliseconds an incremental slice is allowed to run - - // Have a minimum time in seconds to wait between GC slices and before starting a new GC to distribute the GC - // load and to hopefully make it unnoticeable for the player. This value should be high enough to distribute - // the load well enough and low enough to make sure we don't run out of memory before we can start with the - // sweeping. - if (timer_Time() - m_LastGCCheck < delay) - return; - - m_LastGCCheck = timer_Time(); - - int gcBytes = JS_GetGCParameter(m_rt, JSGC_BYTES); - -#if GC_DEBUG_PRINT - std::cout << "gcBytes: " << gcBytes / 1024 << " KB" << std::endl; -#endif - - if (m_LastGCBytes > gcBytes || m_LastGCBytes == 0) - { -#if GC_DEBUG_PRINT - printf("Setting m_LastGCBytes: %d KB \n", gcBytes / 1024); -#endif - m_LastGCBytes = gcBytes; - } - - // Run an additional incremental GC slice if the currently running incremental GC isn't over yet - // ... or - // start a new incremental GC if the JS heap size has grown enough for a GC to make sense - if (JS::IsIncrementalGCInProgress(m_rt) || (gcBytes - m_LastGCBytes > m_HeapGrowthBytesGCTrigger)) - { -#if GC_DEBUG_PRINT - if (JS::IsIncrementalGCInProgress(m_rt)) - printf("An incremental GC cycle is in progress. \n"); - else - printf("GC needed because JSGC_BYTES - m_LastGCBytes > m_HeapGrowthBytesGCTrigger \n" - " JSGC_BYTES: %d KB \n m_LastGCBytes: %d KB \n m_HeapGrowthBytesGCTrigger: %d KB \n", - gcBytes / 1024, - m_LastGCBytes / 1024, - m_HeapGrowthBytesGCTrigger / 1024); -#endif - - // A hack to make sure we never exceed the runtime size because we can't collect the memory - // fast enough. - if (gcBytes > m_RuntimeSize / 2) - { - if (JS::IsIncrementalGCInProgress(m_rt)) - { -#if GC_DEBUG_PRINT - printf("Finishing incremental GC because gcBytes > m_RuntimeSize / 2. \n"); -#endif - PrepareCompartmentsForIncrementalGC(); - JS::FinishIncrementalGC(m_rt, JS::gcreason::REFRESH_FRAME); - } - else - { - if (gcBytes > m_RuntimeSize * 0.75) - { - ShrinkingGC(); -#if GC_DEBUG_PRINT - printf("Running shrinking GC because gcBytes > m_RuntimeSize * 0.75. \n"); -#endif - } - else - { -#if GC_DEBUG_PRINT - printf("Running full GC because gcBytes > m_RuntimeSize / 2. \n"); -#endif - JS_GC(m_rt); - } - } - } - else - { -#if GC_DEBUG_PRINT - if (!JS::IsIncrementalGCInProgress(m_rt)) - printf("Starting incremental GC \n"); - else - printf("Running incremental GC slice \n"); -#endif - PrepareCompartmentsForIncrementalGC(); - if (!JS::IsIncrementalGCInProgress(m_rt)) - JS::StartIncrementalGC(m_rt, GC_NORMAL, JS::gcreason::REFRESH_FRAME, GCSliceTimeBudget); - else - JS::IncrementalGCSlice(m_rt, JS::gcreason::REFRESH_FRAME, GCSliceTimeBudget); - } - m_LastGCBytes = gcBytes; - } - } -} - -void ScriptRuntime::ShrinkingGC() -{ - JS_SetGCParameter(m_rt, JSGC_MODE, JSGC_MODE_COMPARTMENT); - JS::PrepareForFullGC(m_rt); - JS::GCForReason(m_rt, GC_SHRINK, JS::gcreason::REFRESH_FRAME); - JS_SetGCParameter(m_rt, JSGC_MODE, JSGC_MODE_INCREMENTAL); -} - -void ScriptRuntime::PrepareCompartmentsForIncrementalGC() const -{ - for (JSCompartment* const& cmpt : m_Compartments) - JS::PrepareZoneForGC(js::GetCompartmentZone(cmpt)); -} Property changes on: ps/trunk/source/scriptinterface/ScriptRuntime.cpp ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Index: ps/trunk/source/simulation2/components/tests/test_ObstructionManager.h =================================================================== --- ps/trunk/source/simulation2/components/tests/test_ObstructionManager.h (revision 24180) +++ ps/trunk/source/simulation2/components/tests/test_ObstructionManager.h (revision 24181) @@ -1,593 +1,593 @@ /* Copyright (C) 2020 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/ICmpObstruction.h" class MockObstruction : public ICmpObstruction { public: DEFAULT_MOCK_COMPONENT() ICmpObstructionManager::ObstructionSquare obstruction; virtual ICmpObstructionManager::tag_t GetObstruction() const { return ICmpObstructionManager::tag_t(); } virtual bool GetObstructionSquare(ICmpObstructionManager::ObstructionSquare& out) const { out = obstruction; return true; } virtual bool GetPreviousObstructionSquare(ICmpObstructionManager::ObstructionSquare& UNUSED(out)) const { return true; } virtual entity_pos_t GetSize() const { return entity_pos_t::Zero(); } virtual CFixedVector2D GetStaticSize() const { return CFixedVector2D(); } virtual EObstructionType GetObstructionType() const { return ICmpObstruction::STATIC; } virtual void SetUnitClearance(const entity_pos_t& UNUSED(clearance)) { } virtual bool IsControlPersistent() const { return true; } virtual bool CheckShorePlacement() const { return true; } virtual EFoundationCheck CheckFoundation(const std::string& UNUSED(className)) const { return EFoundationCheck(); } virtual EFoundationCheck CheckFoundation(const std::string& UNUSED(className), bool UNUSED(onlyCenterPoint)) const { return EFoundationCheck(); } virtual std::string CheckFoundation_wrapper(const std::string& UNUSED(className), bool UNUSED(onlyCenterPoint)) const { return std::string(); } virtual bool CheckDuplicateFoundation() const { return true; } virtual std::vector GetEntitiesByFlags(ICmpObstructionManager::flags_t UNUSED(flags)) const { return std::vector(); } virtual std::vector GetEntitiesBlockingMovement() const { return std::vector(); } virtual std::vector GetEntitiesBlockingConstruction() const { return std::vector(); } virtual std::vector GetEntitiesDeletedUponConstruction() const { return std::vector(); } virtual void ResolveFoundationCollisions() const { } virtual void SetActive(bool UNUSED(active)) { } virtual void SetMovingFlag(bool UNUSED(enabled)) { } virtual void SetDisableBlockMovementPathfinding(bool UNUSED(movementDisabled), bool UNUSED(pathfindingDisabled), int32_t UNUSED(shape)) { } virtual bool GetBlockMovementFlag() const { return true; } virtual void SetControlGroup(entity_id_t UNUSED(group)) { } virtual entity_id_t GetControlGroup() const { return INVALID_ENTITY; } virtual void SetControlGroup2(entity_id_t UNUSED(group2)) { } virtual entity_id_t GetControlGroup2() const { return INVALID_ENTITY; } }; class TestCmpObstructionManager : public CxxTest::TestSuite { typedef ICmpObstructionManager::tag_t tag_t; typedef ICmpObstructionManager::ObstructionSquare ObstructionSquare; // some variables for setting up a scene with 3 shapes entity_id_t ent1, ent2, ent3; // entity IDs entity_angle_t ent1a; // angles entity_pos_t ent1x, ent1z, ent1w, ent1h, // positions/dimensions ent2x, ent2z, ent2c, ent3x, ent3z, ent3c; entity_id_t ent1g1, ent1g2, ent2g, ent3g; // control groups tag_t shape1, shape2, shape3; ICmpObstructionManager* cmp; ComponentTestHelper* testHelper; public: void setUp() { CXeromyces::Startup(); CxxTest::setAbortTestOnFail(true); // set up a simple scene with some predefined obstruction shapes // (we can't position shapes on the origin because the world bounds must range // from 0 to X, so instead we'll offset things by, say, 10). ent1 = 1; ent1a = fixed::Zero(); ent1w = fixed::FromFloat(4); ent1h = fixed::FromFloat(2); ent1x = fixed::FromInt(10); ent1z = fixed::FromInt(10); ent1g1 = ent1; ent1g2 = INVALID_ENTITY; ent2 = 2; ent2c = fixed::FromFloat(1); ent2x = ent1x; ent2z = ent1z; ent2g = ent1g1; ent3 = 3; ent3c = fixed::FromFloat(3); ent3x = ent2x; ent3z = ent2z + ent2c + ent3c; // ensure it just touches the border of ent2 ent3g = ent3; - testHelper = new ComponentTestHelper(g_ScriptRuntime); + testHelper = new ComponentTestHelper(g_ScriptContext); cmp = testHelper->Add(CID_ObstructionManager, "", SYSTEM_ENTITY); cmp->SetBounds(fixed::FromInt(0), fixed::FromInt(0), fixed::FromInt(1000), fixed::FromInt(1000)); shape1 = cmp->AddStaticShape(ent1, ent1x, ent1z, ent1a, ent1w, ent1h, ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION | ICmpObstructionManager::FLAG_BLOCK_MOVEMENT | ICmpObstructionManager::FLAG_MOVING, ent1g1, ent1g2); shape2 = cmp->AddUnitShape(ent2, ent2x, ent2z, ent2c, ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION | ICmpObstructionManager::FLAG_BLOCK_FOUNDATION, ent2g); shape3 = cmp->AddUnitShape(ent3, ent3x, ent3z, ent3c, ICmpObstructionManager::FLAG_BLOCK_MOVEMENT | ICmpObstructionManager::FLAG_BLOCK_FOUNDATION, ent3g); } void tearDown() { delete testHelper; cmp = NULL; // not our responsibility to deallocate CXeromyces::Terminate(); } /** * Verifies the collision testing procedure. Collision-tests some simple shapes against the shapes registered in * the scene, and verifies the result of the test against the expected value. */ void test_simple_collisions() { std::vector out; NullObstructionFilter nullFilter; // Collision-test a simple shape nested inside shape3 against all shapes in the scene. Since the tested shape // overlaps only with shape 3, we should find only shape 3 in the result. cmp->TestUnitShape(nullFilter, ent3x, ent3z, fixed::FromInt(1), &out); TS_ASSERT_EQUALS(1U, out.size()); TS_ASSERT_EQUALS(ent3, out[0]); out.clear(); cmp->TestStaticShape(nullFilter, ent3x, ent3z, fixed::Zero(), fixed::FromInt(1), fixed::FromInt(1), &out); TS_ASSERT_EQUALS(1U, out.size()); TS_ASSERT_EQUALS(ent3, out[0]); out.clear(); // Similarly, collision-test a simple shape nested inside both shape1 and shape2. Since the tested shape overlaps // only with shapes 1 and 2, those are the only ones we should find in the result. cmp->TestUnitShape(nullFilter, ent2x, ent2z, ent2c/2, &out); TS_ASSERT_EQUALS(2U, out.size()); TS_ASSERT_VECTOR_CONTAINS(out, ent1); TS_ASSERT_VECTOR_CONTAINS(out, ent2); out.clear(); cmp->TestStaticShape(nullFilter, ent2x, ent2z, fixed::Zero(), ent2c, ent2c, &out); TS_ASSERT_EQUALS(2U, out.size()); TS_ASSERT_VECTOR_CONTAINS(out, ent1); TS_ASSERT_VECTOR_CONTAINS(out, ent2); out.clear(); } /** * Verifies the behaviour of the null obstruction filter. Tests with this filter will be performed against all * registered shapes. */ void test_filter_null() { std::vector out; // Collision test a scene-covering shape against all shapes in the scene. We should find all registered shapes // in the result. NullObstructionFilter nullFilter; cmp->TestUnitShape(nullFilter, ent1x, ent1z, fixed::FromInt(10), &out); TS_ASSERT_EQUALS(3U, out.size()); TS_ASSERT_VECTOR_CONTAINS(out, ent1); TS_ASSERT_VECTOR_CONTAINS(out, ent2); TS_ASSERT_VECTOR_CONTAINS(out, ent3); out.clear(); cmp->TestStaticShape(nullFilter, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out); TS_ASSERT_EQUALS(3U, out.size()); TS_ASSERT_VECTOR_CONTAINS(out, ent1); TS_ASSERT_VECTOR_CONTAINS(out, ent2); TS_ASSERT_VECTOR_CONTAINS(out, ent3); out.clear(); } /** * Verifies the behaviour of the StationaryOnlyObstructionFilter. Tests with this filter will be performed only * against non-moving (stationary) shapes. */ void test_filter_stationary_only() { std::vector out; // Collision test a scene-covering shape against all shapes in the scene, but skipping shapes that are moving, // i.e. shapes that have the MOVING flag. Since only shape 1 is flagged as moving, we should find // shapes 2 and 3 in each case. StationaryOnlyObstructionFilter ignoreMoving; cmp->TestUnitShape(ignoreMoving, ent1x, ent1z, fixed::FromInt(10), &out); TS_ASSERT_EQUALS(2U, out.size()); TS_ASSERT_VECTOR_CONTAINS(out, ent2); TS_ASSERT_VECTOR_CONTAINS(out, ent3); out.clear(); cmp->TestStaticShape(ignoreMoving, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out); TS_ASSERT_EQUALS(2U, out.size()); TS_ASSERT_VECTOR_CONTAINS(out, ent2); TS_ASSERT_VECTOR_CONTAINS(out, ent3); out.clear(); } /** * Verifies the behaviour of the SkipTagObstructionFilter. Tests with this filter will be performed against * all registered shapes that do not have the specified tag set. */ void test_filter_skip_tag() { std::vector out; // Collision-test shape 2's obstruction shape against all shapes in the scene, but skipping tests against // shape 2. Since shape 2 overlaps only with shape 1, we should find only shape 1's entity ID in the result. SkipTagObstructionFilter ignoreShape2(shape2); cmp->TestUnitShape(ignoreShape2, ent2x, ent2z, ent2c/2, &out); TS_ASSERT_EQUALS(1U, out.size()); TS_ASSERT_EQUALS(ent1, out[0]); out.clear(); cmp->TestStaticShape(ignoreShape2, ent2x, ent2z, fixed::Zero(), ent2c, ent2c, &out); TS_ASSERT_EQUALS(1U, out.size()); TS_ASSERT_EQUALS(ent1, out[0]); out.clear(); } /** * Verifies the behaviour of the SkipTagFlagsObstructionFilter. Tests with this filter will be performed against * all registered shapes that do not have the specified tag set, and that have at least one of required flags set. */ void test_filter_skip_tag_require_flag() { std::vector out; // Collision-test a scene-covering shape against all shapes in the scene, but skipping tests against shape 1 // and requiring the BLOCK_MOVEMENT flag. Since shape 1 is being ignored and shape 2 does not have the required // flag, we should find only shape 3 in the results. SkipTagRequireFlagsObstructionFilter skipShape1RequireBlockMovement(shape1, ICmpObstructionManager::FLAG_BLOCK_MOVEMENT); cmp->TestUnitShape(skipShape1RequireBlockMovement, ent1x, ent1z, fixed::FromInt(10), &out); TS_ASSERT_EQUALS(1U, out.size()); TS_ASSERT_EQUALS(ent3, out[0]); out.clear(); cmp->TestStaticShape(skipShape1RequireBlockMovement, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out); TS_ASSERT_EQUALS(1U, out.size()); TS_ASSERT_EQUALS(ent3, out[0]); out.clear(); // If we now do the same test, but require at least one of the entire set of available filters, we should find // all shapes that are not shape 1 and that have at least one flag set. Since all shapes in our testing scene // have at least one flag set, we should find shape 2 and shape 3 in the results. SkipTagRequireFlagsObstructionFilter skipShape1RequireAnyFlag(shape1, (ICmpObstructionManager::flags_t) -1); cmp->TestUnitShape(skipShape1RequireAnyFlag, ent1x, ent1z, fixed::FromInt(10), &out); TS_ASSERT_EQUALS(2U, out.size()); TS_ASSERT_VECTOR_CONTAINS(out, ent2); TS_ASSERT_VECTOR_CONTAINS(out, ent3); out.clear(); cmp->TestStaticShape(skipShape1RequireAnyFlag, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out); TS_ASSERT_EQUALS(2U, out.size()); TS_ASSERT_VECTOR_CONTAINS(out, ent2); TS_ASSERT_VECTOR_CONTAINS(out, ent3); out.clear(); // And if we now do the same test yet again, but specify an empty set of flags, then it becomes impossible for // any shape to have at least one of the required flags, and we should hence find no shapes in the result. SkipTagRequireFlagsObstructionFilter skipShape1RejectAll(shape1, 0U); cmp->TestUnitShape(skipShape1RejectAll, ent1x, ent1z, fixed::FromInt(10), &out); TS_ASSERT_EQUALS(0U, out.size()); out.clear(); cmp->TestStaticShape(skipShape1RejectAll, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out); TS_ASSERT_EQUALS(0U, out.size()); out.clear(); } /** * Verifies the behaviour of SkipControlGroupsRequireFlagObstructionFilter. Tests with this filter will be performed * against all registered shapes that are members of neither specified control groups, and that have at least one of * the specified flags set. */ void test_filter_skip_controlgroups_require_flag() { std::vector out; // Collision-test a shape that overlaps the entire scene, but ignoring shapes from shape1's control group // (which also includes shape 2), and requiring that either the BLOCK_FOUNDATION or the // BLOCK_CONSTRUCTION flag is set, or both. Since shape 1 and shape 2 both belong to shape 1's control // group, and shape 3 has the BLOCK_FOUNDATION flag (but not BLOCK_CONSTRUCTION), we should find only // shape 3 in the result. SkipControlGroupsRequireFlagObstructionFilter skipGroup1ReqFoundConstr(ent1g1, INVALID_ENTITY, ICmpObstructionManager::FLAG_BLOCK_FOUNDATION | ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION); cmp->TestUnitShape(skipGroup1ReqFoundConstr, ent1x, ent1z, fixed::FromInt(10), &out); TS_ASSERT_EQUALS(1U, out.size()); TS_ASSERT_EQUALS(ent3, out[0]); out.clear(); cmp->TestStaticShape(skipGroup1ReqFoundConstr, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out); TS_ASSERT_EQUALS(1U, out.size()); TS_ASSERT_EQUALS(ent3, out[0]); out.clear(); // Perform the same test, but now also exclude shape 3's control group (in addition to shape 1's control // group). Despite shape 3 having at least one of the required flags set, it should now also be ignored, // yielding an empty result set. SkipControlGroupsRequireFlagObstructionFilter skipGroup1And3ReqFoundConstr(ent1g1, ent3g, ICmpObstructionManager::FLAG_BLOCK_FOUNDATION | ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION); cmp->TestUnitShape(skipGroup1And3ReqFoundConstr, ent1x, ent1z, fixed::FromInt(10), &out); TS_ASSERT_EQUALS(0U, out.size()); out.clear(); cmp->TestStaticShape(skipGroup1And3ReqFoundConstr, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out); TS_ASSERT_EQUALS(0U, out.size()); out.clear(); // Same test, but this time excluding only shape 3's control group, and requiring any of the available flags // to be set. Since both shape 1 and shape 2 have at least one flag set and are both in a different control // group, we should find them in the result. SkipControlGroupsRequireFlagObstructionFilter skipGroup3RequireAnyFlag(ent3g, INVALID_ENTITY, (ICmpObstructionManager::flags_t) -1); cmp->TestUnitShape(skipGroup3RequireAnyFlag, ent1x, ent1z, fixed::FromInt(10), &out); TS_ASSERT_EQUALS(2U, out.size()); TS_ASSERT_VECTOR_CONTAINS(out, ent1); TS_ASSERT_VECTOR_CONTAINS(out, ent2); out.clear(); cmp->TestStaticShape(skipGroup3RequireAnyFlag, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out); TS_ASSERT_EQUALS(2U, out.size()); TS_ASSERT_VECTOR_CONTAINS(out, ent1); TS_ASSERT_VECTOR_CONTAINS(out, ent2); out.clear(); // Finally, the same test as the one directly above, now with an empty set of required flags. Since it now becomes // impossible for shape 1 and shape 2 to have at least one of the required flags set, and shape 3 is excluded by // virtue of the control group filtering, we should find an empty result. SkipControlGroupsRequireFlagObstructionFilter skipGroup3RequireNoFlags(ent3g, INVALID_ENTITY, 0U); cmp->TestUnitShape(skipGroup3RequireNoFlags, ent1x, ent1z, fixed::FromInt(10), &out); TS_ASSERT_EQUALS(0U, out.size()); out.clear(); cmp->TestStaticShape(skipGroup3RequireNoFlags, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out); TS_ASSERT_EQUALS(0U, out.size()); out.clear(); // ------------------------------------------------------------------------------------ // In the tests up until this point, the shapes have all been filtered out based on their primary control group. // Now, to verify that shapes are also filtered out based on their secondary control groups, add a fourth shape // with arbitrarily-chosen dual control groups, and also change shape 1's secondary control group to another // arbitrarily-chosen control group. Then, do a scene-covering collision test while filtering out a combination // of shape 1's secondary control group, and one of shape 4's control groups. We should find neither ent1 nor ent4 // in the result. entity_id_t ent4 = 4, ent4g1 = 17, ent4g2 = 19, ent1g2_new = 18; // new secondary control group for entity 1 entity_pos_t ent4x = fixed::FromInt(4), ent4z = fixed::Zero(), ent4w = fixed::FromInt(1), ent4h = fixed::FromInt(1); entity_angle_t ent4a = fixed::FromDouble(M_PI/3); cmp->AddStaticShape(ent4, ent4x, ent4z, ent4a, ent4w, ent4h, ICmpObstructionManager::FLAG_BLOCK_PATHFINDING, ent4g1, ent4g2); cmp->SetStaticControlGroup(shape1, ent1g1, ent1g2_new); // Exclude shape 1's and shape 4's secondary control groups from testing, and require any available flag to be set. // Since neither shape 2 nor shape 3 are part of those control groups and both have at least one available flag set, // the results should only those two shapes' entities. SkipControlGroupsRequireFlagObstructionFilter skipGroup1SecAnd4SecRequireAny(ent1g2_new, ent4g2, (ICmpObstructionManager::flags_t) -1); cmp->TestUnitShape(skipGroup1SecAnd4SecRequireAny, ent1x, ent1z, fixed::FromInt(10), &out); TS_ASSERT_EQUALS(2U, out.size()); TS_ASSERT_VECTOR_CONTAINS(out, ent2); TS_ASSERT_VECTOR_CONTAINS(out, ent3); out.clear(); cmp->TestStaticShape(skipGroup1SecAnd4SecRequireAny, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out); TS_ASSERT_EQUALS(2U, out.size()); TS_ASSERT_VECTOR_CONTAINS(out, ent2); TS_ASSERT_VECTOR_CONTAINS(out, ent3); out.clear(); // Same as the above, but now exclude shape 1's secondary and shape 4's primary control group, while still requiring // any available flag to be set. (Note that the test above used shape 4's secondary control group). Results should // remain the same. SkipControlGroupsRequireFlagObstructionFilter skipGroup1SecAnd4PrimRequireAny(ent1g2_new, ent4g1, (ICmpObstructionManager::flags_t) -1); cmp->TestUnitShape(skipGroup1SecAnd4PrimRequireAny, ent1x, ent1z, fixed::FromInt(10), &out); TS_ASSERT_EQUALS(2U, out.size()); TS_ASSERT_VECTOR_CONTAINS(out, ent2); TS_ASSERT_VECTOR_CONTAINS(out, ent3); out.clear(); cmp->TestStaticShape(skipGroup1SecAnd4PrimRequireAny, ent1x, ent1z, fixed::Zero(), fixed::FromInt(10), fixed::FromInt(10), &out); TS_ASSERT_EQUALS(2U, out.size()); TS_ASSERT_VECTOR_CONTAINS(out, ent2); TS_ASSERT_VECTOR_CONTAINS(out, ent3); out.clear(); cmp->SetStaticControlGroup(shape1, ent1g1, ent1g2); // restore shape 1's original secondary control group } void test_adjacent_shapes() { std::vector out; NullObstructionFilter nullFilter; SkipTagObstructionFilter ignoreShape1(shape1); SkipTagObstructionFilter ignoreShape2(shape2); SkipTagObstructionFilter ignoreShape3(shape3); // Collision-test a shape that is perfectly adjacent to shape3. This should be counted as a hit according to // the code at the time of writing. entity_angle_t ent4a = fixed::FromDouble(M_PI); // rotated 180 degrees, should not affect collision test entity_pos_t ent4w = fixed::FromInt(2), ent4h = fixed::FromInt(1), ent4x = ent3x + ent3c + ent4w/2, // make ent4 adjacent to ent3 ent4z = ent3z; cmp->TestStaticShape(nullFilter, ent4x, ent4z, ent4a, ent4w, ent4h, &out); TS_ASSERT_EQUALS(1U, out.size()); TS_ASSERT_EQUALS(ent3, out[0]); out.clear(); cmp->TestUnitShape(nullFilter, ent4x, ent4z, ent4w/2, &out); TS_ASSERT_EQUALS(1U, out.size()); TS_ASSERT_EQUALS(ent3, out[0]); out.clear(); // now do the same tests, but move the shape a little bit to the right so that it doesn't touch anymore cmp->TestStaticShape(nullFilter, ent4x + fixed::FromFloat(1e-5f), ent4z, ent4a, ent4w, ent4h, &out); TS_ASSERT_EQUALS(0U, out.size()); out.clear(); cmp->TestUnitShape(nullFilter, ent4x + fixed::FromFloat(1e-5f), ent4z, ent4w/2, &out); TS_ASSERT_EQUALS(0U, out.size()); out.clear(); } /** * Verifies that fetching the registered shapes from the obstruction manager yields the correct results. */ void test_get_obstruction() { ObstructionSquare obSquare1 = cmp->GetObstruction(shape1); ObstructionSquare obSquare2 = cmp->GetObstruction(shape2); ObstructionSquare obSquare3 = cmp->GetObstruction(shape3); TS_ASSERT_EQUALS(obSquare1.hh, ent1h/2); TS_ASSERT_EQUALS(obSquare1.hw, ent1w/2); TS_ASSERT_EQUALS(obSquare1.x, ent1x); TS_ASSERT_EQUALS(obSquare1.z, ent1z); TS_ASSERT_EQUALS(obSquare1.u, CFixedVector2D(fixed::FromInt(1), fixed::FromInt(0))); TS_ASSERT_EQUALS(obSquare1.v, CFixedVector2D(fixed::FromInt(0), fixed::FromInt(1))); TS_ASSERT_EQUALS(obSquare2.hh, ent2c); TS_ASSERT_EQUALS(obSquare2.hw, ent2c); TS_ASSERT_EQUALS(obSquare2.x, ent2x); TS_ASSERT_EQUALS(obSquare2.z, ent2z); TS_ASSERT_EQUALS(obSquare2.u, CFixedVector2D(fixed::FromInt(1), fixed::FromInt(0))); TS_ASSERT_EQUALS(obSquare2.v, CFixedVector2D(fixed::FromInt(0), fixed::FromInt(1))); TS_ASSERT_EQUALS(obSquare3.hh, ent3c); TS_ASSERT_EQUALS(obSquare3.hw, ent3c); TS_ASSERT_EQUALS(obSquare3.x, ent3x); TS_ASSERT_EQUALS(obSquare3.z, ent3z); TS_ASSERT_EQUALS(obSquare3.u, CFixedVector2D(fixed::FromInt(1), fixed::FromInt(0))); TS_ASSERT_EQUALS(obSquare3.v, CFixedVector2D(fixed::FromInt(0), fixed::FromInt(1))); } /** * Verifies the calculations of distances between shapes. */ void test_distance_to() { // Create two more entities to have non-zero distances entity_id_t ent4 = 4, ent4g1 = ent4, ent4g2 = INVALID_ENTITY, ent5 = 5, ent5g1 = ent5, ent5g2 = INVALID_ENTITY; entity_pos_t ent4a = fixed::Zero(), ent4w = fixed::FromInt(6), ent4h = fixed::Zero(), ent4x = ent1x, ent4z = fixed::FromInt(20), ent5a = fixed::Zero(), ent5w = fixed::FromInt(2), ent5h = fixed::FromInt(4), ent5x = fixed::FromInt(20), ent5z = ent1z; tag_t shape4 = cmp->AddStaticShape(ent4, ent4x, ent4z, ent4a, ent4w, ent4h, ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION | ICmpObstructionManager::FLAG_BLOCK_MOVEMENT | ICmpObstructionManager::FLAG_MOVING, ent4g1, ent4g2); tag_t shape5 = cmp->AddStaticShape(ent5, ent5x, ent5z, ent5a, ent5w, ent5h, ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION | ICmpObstructionManager::FLAG_BLOCK_MOVEMENT | ICmpObstructionManager::FLAG_MOVING, ent5g1, ent5g2); MockObstruction obstruction1, obstruction2, obstruction3, obstruction4, obstruction5; testHelper->AddMock(ent1, IID_Obstruction, obstruction1); testHelper->AddMock(ent2, IID_Obstruction, obstruction2); testHelper->AddMock(ent3, IID_Obstruction, obstruction3); testHelper->AddMock(ent4, IID_Obstruction, obstruction4); testHelper->AddMock(ent5, IID_Obstruction, obstruction5); obstruction1.obstruction = cmp->GetObstruction(shape1); obstruction2.obstruction = cmp->GetObstruction(shape2); obstruction3.obstruction = cmp->GetObstruction(shape3); obstruction4.obstruction = cmp->GetObstruction(shape4); obstruction5.obstruction = cmp->GetObstruction(shape5); TS_ASSERT_EQUALS(fixed::Zero(), cmp->DistanceToTarget(ent1, ent2)); TS_ASSERT_EQUALS(fixed::Zero(), cmp->DistanceToTarget(ent2, ent1)); TS_ASSERT_EQUALS(fixed::Zero(), cmp->DistanceToTarget(ent2, ent3)); TS_ASSERT_EQUALS(fixed::Zero(), cmp->DistanceToTarget(ent3, ent2)); // Due to rounding errors we need to use some leeway TS_ASSERT_DELTA(fixed::FromFloat(std::sqrt(80)), cmp->MaxDistanceToTarget(ent2, ent3), fixed::FromFloat(0.0001)); TS_ASSERT_DELTA(fixed::FromFloat(std::sqrt(80)), cmp->MaxDistanceToTarget(ent3, ent2), fixed::FromFloat(0.0001)); TS_ASSERT_EQUALS(fixed::Zero(), cmp->DistanceToTarget(ent1, ent3)); TS_ASSERT_EQUALS(fixed::Zero(), cmp->DistanceToTarget(ent3, ent1)); TS_ASSERT_EQUALS(fixed::FromInt(6), cmp->DistanceToTarget(ent1, ent4)); TS_ASSERT_EQUALS(fixed::FromInt(6), cmp->DistanceToTarget(ent4, ent1)); TS_ASSERT_DELTA(fixed::FromFloat(std::sqrt(125) + 3), cmp->MaxDistanceToTarget(ent1, ent4), fixed::FromFloat(0.0001)); TS_ASSERT_DELTA(fixed::FromFloat(std::sqrt(125) + 3), cmp->MaxDistanceToTarget(ent4, ent1), fixed::FromFloat(0.0001)); TS_ASSERT_EQUALS(fixed::FromInt(7), cmp->DistanceToTarget(ent1, ent5)); TS_ASSERT_EQUALS(fixed::FromInt(7), cmp->DistanceToTarget(ent5, ent1)); TS_ASSERT_DELTA(fixed::FromFloat(std::sqrt(178)), cmp->MaxDistanceToTarget(ent1, ent5), fixed::FromFloat(0.0001)); TS_ASSERT_DELTA(fixed::FromFloat(std::sqrt(178)), cmp->MaxDistanceToTarget(ent5, ent1), fixed::FromFloat(0.0001)); TS_ASSERT(cmp->IsInTargetRange(ent1, ent2, fixed::Zero(), fixed::FromInt(1), true)); TS_ASSERT(cmp->IsInTargetRange(ent1, ent2, fixed::Zero(), fixed::FromInt(1), false)); TS_ASSERT(cmp->IsInTargetRange(ent1, ent2, fixed::FromInt(1), fixed::FromInt(1), true)); TS_ASSERT(!cmp->IsInTargetRange(ent1, ent2, fixed::FromInt(1), fixed::FromInt(1), false)); TS_ASSERT(cmp->IsInTargetRange(ent1, ent5, fixed::Zero(), fixed::FromInt(10), true)); TS_ASSERT(cmp->IsInTargetRange(ent1, ent5, fixed::Zero(), fixed::FromInt(10), false)); TS_ASSERT(cmp->IsInTargetRange(ent1, ent5, fixed::FromInt(1), fixed::FromInt(10), true)); TS_ASSERT(!cmp->IsInTargetRange(ent1, ent5, fixed::FromInt(1), fixed::FromInt(5), false)); TS_ASSERT(!cmp->IsInTargetRange(ent1, ent5, fixed::FromInt(10), fixed::FromInt(10), false)); TS_ASSERT(cmp->IsInTargetRange(ent1, ent5, fixed::FromInt(10), fixed::FromInt(10), true)); } }; Index: ps/trunk/source/simulation2/components/tests/test_RangeManager.h =================================================================== --- ps/trunk/source/simulation2/components/tests/test_RangeManager.h (revision 24180) +++ ps/trunk/source/simulation2/components/tests/test_RangeManager.h (revision 24181) @@ -1,156 +1,156 @@ /* Copyright (C) 2019 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/ICmpRangeManager.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpVision.h" #include #include class MockVision : public ICmpVision { public: DEFAULT_MOCK_COMPONENT() virtual entity_pos_t GetRange() const { return entity_pos_t::FromInt(66); } virtual bool GetRevealShore() const { return false; } }; class MockPosition : public ICmpPosition { public: DEFAULT_MOCK_COMPONENT() virtual void SetTurretParent(entity_id_t UNUSED(id), const CFixedVector3D& UNUSED(pos)) {} virtual entity_id_t GetTurretParent() const {return INVALID_ENTITY;} virtual void UpdateTurretPosition() {} virtual std::set* GetTurrets() { return NULL; } virtual bool IsInWorld() const { return true; } virtual void MoveOutOfWorld() { } virtual void MoveTo(entity_pos_t UNUSED(x), entity_pos_t UNUSED(z)) { } virtual void MoveAndTurnTo(entity_pos_t UNUSED(x), entity_pos_t UNUSED(z), entity_angle_t UNUSED(a)) { } virtual void JumpTo(entity_pos_t UNUSED(x), entity_pos_t UNUSED(z)) { } virtual void SetHeightOffset(entity_pos_t UNUSED(dy)) { } virtual entity_pos_t GetHeightOffset() const { return entity_pos_t::Zero(); } virtual void SetHeightFixed(entity_pos_t UNUSED(y)) { } virtual entity_pos_t GetHeightFixed() const { return entity_pos_t::Zero(); } virtual bool IsHeightRelative() const { return true; } virtual void SetHeightRelative(bool UNUSED(relative)) { } virtual bool CanFloat() const { return false; } virtual void SetFloating(bool UNUSED(flag)) { } virtual void SetActorFloating(bool UNUSED(flag)) { } virtual void SetConstructionProgress(fixed UNUSED(progress)) { } virtual CFixedVector3D GetPosition() const { return CFixedVector3D(); } virtual CFixedVector2D GetPosition2D() const { return CFixedVector2D(); } virtual CFixedVector3D GetPreviousPosition() const { return CFixedVector3D(); } virtual CFixedVector2D GetPreviousPosition2D() const { return CFixedVector2D(); } virtual void TurnTo(entity_angle_t UNUSED(y)) { } virtual void SetYRotation(entity_angle_t UNUSED(y)) { } virtual void SetXZRotation(entity_angle_t UNUSED(x), entity_angle_t UNUSED(z)) { } virtual CFixedVector3D GetRotation() const { return CFixedVector3D(); } virtual fixed GetDistanceTravelled() const { return fixed::Zero(); } virtual void GetInterpolatedPosition2D(float UNUSED(frameOffset), float& x, float& z, float& rotY) const { x = z = rotY = 0; } virtual CMatrix3D GetInterpolatedTransform(float UNUSED(frameOffset)) const { return CMatrix3D(); } }; class TestCmpRangeManager : public CxxTest::TestSuite { public: void setUp() { CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); } // TODO It would be nice to call Verify() with the shore revealing system // but that means testing on an actual map, with water and land. void test_basic() { - ComponentTestHelper test(g_ScriptRuntime); + ComponentTestHelper test(g_ScriptContext); ICmpRangeManager* cmp = test.Add(CID_RangeManager, "", SYSTEM_ENTITY); MockVision vision; test.AddMock(100, IID_Vision, vision); MockPosition position; test.AddMock(100, IID_Position, position); // This tests that the incremental computation produces the correct result // in various edge cases cmp->SetBounds(entity_pos_t::FromInt(0), entity_pos_t::FromInt(0), entity_pos_t::FromInt(512), entity_pos_t::FromInt(512), 512/TERRAIN_TILE_SIZE + 1); cmp->Verify(); { CMessageCreate msg(100); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessageOwnershipChanged msg(100, -1, 1); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(247), entity_pos_t::FromDouble(257.95), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(247), entity_pos_t::FromInt(253), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256), entity_pos_t::FromInt(256), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256)+entity_pos_t::Epsilon(), entity_pos_t::FromInt(256), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256)-entity_pos_t::Epsilon(), entity_pos_t::FromInt(256), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256), entity_pos_t::FromInt(256)+entity_pos_t::Epsilon(), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256), entity_pos_t::FromInt(256)-entity_pos_t::Epsilon(), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(383), entity_pos_t::FromInt(84), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(348), entity_pos_t::FromInt(83), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); boost::mt19937 rng; for (size_t i = 0; i < 1024; ++i) { double x = boost::random::uniform_real_distribution(0.0, 512.0)(rng); double z = boost::random::uniform_real_distribution(0.0, 512.0)(rng); { CMessagePositionChanged msg(100, true, entity_pos_t::FromDouble(x), entity_pos_t::FromDouble(z), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); } // Test OwnershipChange, GetEntitiesByPlayer, GetNonGaiaEntities { player_id_t previousOwner = -1; for (player_id_t newOwner = 0; newOwner < 8; ++newOwner) { CMessageOwnershipChanged msg(100, previousOwner, newOwner); cmp->HandleMessage(msg, false); for (player_id_t i = 0; i < 8; ++i) TS_ASSERT_EQUALS(cmp->GetEntitiesByPlayer(i).size(), i == newOwner ? 1 : 0); TS_ASSERT_EQUALS(cmp->GetNonGaiaEntities().size(), newOwner > 0 ? 1 : 0); previousOwner = newOwner; } } } }; Index: ps/trunk/source/simulation2/system/ComponentManager.h =================================================================== --- ps/trunk/source/simulation2/system/ComponentManager.h (revision 24180) +++ ps/trunk/source/simulation2/system/ComponentManager.h (revision 24181) @@ -1,342 +1,342 @@ /* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_COMPONENTMANAGER #define INCLUDED_COMPONENTMANAGER #include "ps/Filesystem.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/helpers/Player.h" #include "simulation2/system/Components.h" #include "simulation2/system/Entity.h" #include #include #include #include class IComponent; class CParamNode; class CMessage; class CSimContext; class CDynamicSubscription; class CComponentManager { NONCOPYABLE(CComponentManager); public: // We can't use EInterfaceId/etc directly, since scripts dynamically generate new IDs // and casting arbitrary ints to enums is undefined behaviour, so use 'int' typedefs typedef int InterfaceId; typedef int ComponentTypeId; typedef int MessageTypeId; private: // Component allocation types typedef IComponent* (*AllocFunc)(const ScriptInterface& scriptInterface, JS::HandleValue ctor); typedef void (*DeallocFunc)(IComponent*); // ComponentTypes come in three types: // Native: normal C++ component // ScriptWrapper: C++ component that wraps a JS component implementation // Script: a ScriptWrapper linked to a specific JS component implementation enum EComponentTypeType { CT_Native, CT_ScriptWrapper, CT_Script }; // Representation of a component type, to be used when instantiating components struct ComponentType { EComponentTypeType type; InterfaceId iid; AllocFunc alloc; DeallocFunc dealloc; std::string name; std::string schema; // RelaxNG fragment std::unique_ptr ctor; // only valid if type == CT_Script }; public: - CComponentManager(CSimContext&, shared_ptr rt, bool skipScriptFunctions = false); + CComponentManager(CSimContext&, shared_ptr rt, bool skipScriptFunctions = false); ~CComponentManager(); void LoadComponentTypes(); /** * Load a script and execute it in a new function scope. * @param filename VFS path to load * @param hotload set to true if this script has been loaded before, and redefinitions of * existing components should not be considered errors */ bool LoadScript(const VfsPath& filename, bool hotload = false); void RegisterMessageType(MessageTypeId mtid, const char* name); void RegisterComponentType(InterfaceId, ComponentTypeId, AllocFunc, DeallocFunc, const char*, const std::string& schema); void RegisterComponentTypeScriptWrapper(InterfaceId, ComponentTypeId, AllocFunc, DeallocFunc, const char*, const std::string& schema); void MarkScriptedComponentForSystemEntity(CComponentManager::ComponentTypeId cid); /** * Subscribe the current component type to the given message type. * Each component's HandleMessage will be called on any BroadcastMessage of this message type, * or on any PostMessage of this type targeted at the component's entity. * Must only be called by a component type's ClassInit. */ void SubscribeToMessageType(MessageTypeId mtid); /** * Subscribe the current component type to all messages of the given message type. * Each component's HandleMessage will be called on any BroadcastMessage or PostMessage of this message type, * regardless of the entity. * Must only be called by a component type's ClassInit. */ void SubscribeGloballyToMessageType(MessageTypeId mtid); /** * Subscribe the given component instance to all messages of the given message type. * The component's HandleMessage will be called on any BroadcastMessage or PostMessage of * this message type, regardless of the entity. * * This can be called at any time (including inside the HandleMessage callback for this message type). * * The component type must not have statically subscribed to this message type in its ClassInit. * * The subscription status is not saved or network-synchronised. Components must remember to * resubscribe in their Deserialize methods if they still want the message. * * This is primarily intended for Interpolate and RenderSubmit messages, to avoid the cost of * sending the message to components that do not currently need to do any rendering. */ void DynamicSubscriptionNonsync(MessageTypeId mtid, IComponent* component, bool enabled); /** * @param cname Requested component type name (not including any "CID" or "CCmp" prefix) * @return The component type id, or CID__Invalid if not found */ ComponentTypeId LookupCID(const std::string& cname) const; /** * @return The name of the given component type, or "" if not found */ std::string LookupComponentTypeName(ComponentTypeId cid) const; /** * Set up an empty SYSTEM_ENTITY. Must be called after ResetState() and before GetSystemEntity(). */ void InitSystemEntity(); /** * Returns a CEntityHandle with id SYSTEM_ENTITY. */ CEntityHandle GetSystemEntity() { ASSERT(m_SystemEntity.GetId() == SYSTEM_ENTITY); return m_SystemEntity; } /** * Returns a CEntityHandle with id @p ent. * If @p allowCreate is true and there is no existing CEntityHandle, a new handle will be allocated. */ CEntityHandle LookupEntityHandle(entity_id_t ent, bool allowCreate = false); /** * Returns true if the entity with id @p ent exists. */ bool EntityExists(entity_id_t ent) const; /** * Returns a new entity ID that has never been used before. * This affects the simulation state so it must only be called in network-synchronised ways. */ entity_id_t AllocateNewEntity(); /** * Returns a new local entity ID that has never been used before. * This entity will not be synchronised over the network, stored in saved games, etc. */ entity_id_t AllocateNewLocalEntity(); /** * Returns a new entity ID that has never been used before. * If possible, returns preferredId, and ensures this ID won't be allocated again. * This affects the simulation state so it must only be called in network-synchronised ways. */ entity_id_t AllocateNewEntity(entity_id_t preferredId); /** * Constructs a component of type 'cid', initialised with data 'paramNode', * and attaches it to entity 'ent'. * * @return true on success; false on failure, and logs an error message */ bool AddComponent(CEntityHandle ent, ComponentTypeId cid, const CParamNode& paramNode); /** * Add all system components to the system entity (skip the scripted components or the AI components on demand) */ void AddSystemComponents(bool skipScriptedComponents, bool skipAI); /** * Adds an externally-created component, so that it is returned by QueryInterface * but does not get destroyed and does not receive messages from the component manager. * (This is intended for unit tests that need to add mock objects the tested components * expect to exist.) */ void AddMockComponent(CEntityHandle ent, InterfaceId iid, IComponent& component); /** * Allocates a component object of type 'cid', and attaches it to entity 'ent'. * (The component's Init is not called here - either Init or Deserialize must be called * before using the returned object.) */ IComponent* ConstructComponent(CEntityHandle ent, ComponentTypeId cid); /** * Constructs an entity based on the given template, and adds it the world with * entity ID @p ent. There should not be any existing components with that entity ID. * @return ent, or INVALID_ENTITY on error */ entity_id_t AddEntity(const std::wstring& templateName, entity_id_t ent); /** * Destroys all the components belonging to the specified entity when FlushDestroyedComponents is called. * Has no effect if the entity does not exist, or has already been added to the destruction queue. */ void DestroyComponentsSoon(entity_id_t ent); /** * Does the actual destruction of components from DestroyComponentsSoon. * This must not be called if the component manager is on the call stack (since it * will break internal iterators). */ void FlushDestroyedComponents(); IComponent* QueryInterface(entity_id_t ent, InterfaceId iid) const; using InterfacePair = std::pair; using InterfaceList = std::vector; using InterfaceListUnordered = std::unordered_map; InterfaceList GetEntitiesWithInterface(InterfaceId iid) const; const InterfaceListUnordered& GetEntitiesWithInterfaceUnordered(InterfaceId iid) const; /** * Send a message, targeted at a particular entity. The message will be received by any * components of that entity which subscribed to the message type, and by any other components * that subscribed globally to the message type. */ void PostMessage(entity_id_t ent, const CMessage& msg); /** * Send a message, not targeted at any particular entity. The message will be received by any * components that subscribed (either globally or not) to the message type. */ void BroadcastMessage(const CMessage& msg); /** * Resets the dynamic simulation state (deletes all entities, resets entity ID counters; * doesn't unload/reload component scripts). */ void ResetState(); /** * Initializes the random number generator with a seed determined by the host. */ void SetRNGSeed(u32 seed); // Various state serialization functions: bool ComputeStateHash(std::string& outHash, bool quick) const; bool DumpDebugState(std::ostream& stream, bool includeDebugInfo) const; // FlushDestroyedComponents must be called before SerializeState (since the destruction queue // won't get serialized) bool SerializeState(std::ostream& stream) const; bool DeserializeState(std::istream& stream); std::string GenerateSchema() const; ScriptInterface& GetScriptInterface() { return m_ScriptInterface; } private: // Implementations of functions exposed to scripts static void Script_RegisterComponentType_Common(ScriptInterface::CmptPrivate* pCmptPrivate, int iid, const std::string& cname, JS::HandleValue ctor, bool reRegister, bool systemComponent); static void Script_RegisterComponentType(ScriptInterface::CmptPrivate* pCmptPrivate, int iid, const std::string& cname, JS::HandleValue ctor); static void Script_RegisterSystemComponentType(ScriptInterface::CmptPrivate* pCmptPrivate, int iid, const std::string& cname, JS::HandleValue ctor); static void Script_ReRegisterComponentType(ScriptInterface::CmptPrivate* pCmptPrivate, int iid, const std::string& cname, JS::HandleValue ctor); static void Script_RegisterInterface(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& name); static void Script_RegisterMessageType(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& name); static void Script_RegisterGlobal(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& name, JS::HandleValue value); static IComponent* Script_QueryInterface(ScriptInterface::CmptPrivate* pCmptPrivate, int ent, int iid); static std::vector Script_GetEntitiesWithInterface(ScriptInterface::CmptPrivate* pCmptPrivate, int iid); static std::vector Script_GetComponentsWithInterface(ScriptInterface::CmptPrivate* pCmptPrivate, int iid); static void Script_PostMessage(ScriptInterface::CmptPrivate* pCmptPrivate, int ent, int mtid, JS::HandleValue data); static void Script_BroadcastMessage(ScriptInterface::CmptPrivate* pCmptPrivate, int mtid, JS::HandleValue data); static int Script_AddEntity(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& templateName); static int Script_AddLocalEntity(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& templateName); static void Script_DestroyEntity(ScriptInterface::CmptPrivate* pCmptPrivate, int ent); static void Script_FlushDestroyedEntities(ScriptInterface::CmptPrivate* pCmptPrivate); CMessage* ConstructMessage(int mtid, JS::HandleValue data); void SendGlobalMessage(entity_id_t ent, const CMessage& msg); void FlattenDynamicSubscriptions(); void RemoveComponentDynamicSubscriptions(IComponent* component); ComponentTypeId GetScriptWrapper(InterfaceId iid); CEntityHandle AllocateEntityHandle(entity_id_t ent); ScriptInterface m_ScriptInterface; CSimContext& m_SimContext; CEntityHandle m_SystemEntity; ComponentTypeId m_CurrentComponent; // used when loading component types bool m_CurrentlyHotloading; // TODO: some of these should be vectors std::map m_ComponentTypesById; std::vector m_ScriptedSystemComponents; std::vector > m_ComponentsByInterface; // indexed by InterfaceId std::map > m_ComponentsByTypeId; std::map > m_LocalMessageSubscriptions; std::map > m_GlobalMessageSubscriptions; std::map m_ComponentTypeIdsByName; std::map m_MessageTypeIdsByName; std::map m_MessageTypeNamesById; std::map m_InterfaceIdsByName; std::map m_DynamicMessageSubscriptionsNonsync; std::map > m_DynamicMessageSubscriptionsNonsyncByComponent; std::unordered_map m_ComponentCaches; // TODO: maintaining both ComponentsBy* is nasty; can we get rid of one, // while keeping QueryInterface and PostMessage sufficiently efficient? std::vector m_DestructionQueue; ComponentTypeId m_NextScriptComponentTypeId; entity_id_t m_NextEntityId; entity_id_t m_NextLocalEntityId; boost::rand48 m_RNG; friend class TestComponentManager; }; #endif // INCLUDED_COMPONENTMANAGER Index: ps/trunk/source/simulation2/tests/test_ComponentManager.h =================================================================== --- ps/trunk/source/simulation2/tests/test_ComponentManager.h (revision 24180) +++ ps/trunk/source/simulation2/tests/test_ComponentManager.h (revision 24181) @@ -1,904 +1,904 @@ /* Copyright (C) 2019 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")); CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); } void test_Load() { CSimContext context; - CComponentManager man(context, g_ScriptRuntime); + CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); } void test_LookupCID() { CSimContext context; - CComponentManager man(context, g_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + CComponentManager man(context, g_ScriptContext); ScriptTestSetup(man.m_ScriptInterface); man.LoadComponentTypes(); { TestLogger log; TS_ASSERT(man.LoadScript(L"simulation/components/error.js")); // In SpiderMonkey 1.6, JS_ReportError calls the error reporter even if it's inside // a try{} in the script; in recent versions (not sure when it changed) it doesn't // so the error here won't get reported. TS_ASSERT_STR_NOT_CONTAINS(log.GetOutput(), "ERROR: JavaScript error: simulation/components/error.js line 4\nInvalid interface id"); } } void test_script_entityID() { CSimContext context; - CComponentManager man(context, g_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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\ object: {\n\ \"x\": 1234,\n\ \"str\": \"this is a string\",\n\ \"things\": {\n\ \"a\": 1,\n\ \"b\": \"2\",\n\ \"c\": [\n\ 3,\n\ \"4\",\n\ [\n\ 5,\n\ []\n\ ]\n\ ]\n\ }\n\ }\n\ \n\ - id: 2\n\ TestScript1_entity:\n\ object: {}\n\ \n\ - id: 3\n\ TestScript1_nontree:\n\ object: ({x:[[2], [2], [], {y:[2]}]})\n\ \n\ - id: 4\n\ TestScript1_custom:\n\ object: {\n\ \"c\": 1\n\ }\n\ \n" ); std::stringstream stateStream; TS_ASSERT(man.SerializeState(stateStream)); CSimContext context2; - CComponentManager man2(context2, g_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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 24180) +++ ps/trunk/source/simulation2/tests/test_Simulation2.h (revision 24181) @@ -1,171 +1,171 @@ /* Copyright (C) 2019 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")); CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); } void test_AddEntity() { - CSimulation2 sim(NULL, g_ScriptRuntime, &m_Terrain); + 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_ScriptRuntime, &m_Terrain); + 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_ScriptRuntime, &m_Terrain); + 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/scriptinterface/ScriptInterface.cpp =================================================================== --- ps/trunk/source/scriptinterface/ScriptInterface.cpp (revision 24180) +++ ps/trunk/source/scriptinterface/ScriptInterface.cpp (revision 24181) @@ -1,1061 +1,1061 @@ /* Copyright (C) 2020 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 "ScriptContext.h" #include "ScriptInterface.h" -#include "ScriptRuntime.h" #include "ScriptStats.h" #include "lib/debug.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Profile.h" #include "ps/utf16string.h" #include #include #define BOOST_MULTI_INDEX_DISABLE_SERIALIZATION #include #include #include #include #include #include #include #include "valgrind.h" #include "scriptinterface/ScriptExtraHeaders.h" /** * @file * Abstractions of various SpiderMonkey features. * Engine code should be using functions of these interfaces rather than * directly accessing the underlying JS api. */ struct ScriptInterface_impl { - ScriptInterface_impl(const char* nativeScopeName, const shared_ptr& runtime); + ScriptInterface_impl(const char* nativeScopeName, const shared_ptr& context); ~ScriptInterface_impl(); void Register(const char* name, JSNative fptr, uint nargs) const; // Take care to keep this declaration before heap rooted members. Destructors of heap rooted - // members have to be called before the runtime destructor. - shared_ptr m_runtime; + // members have to be called before the context destructor. + shared_ptr m_context; friend ScriptInterface::Request; private: JSContext* m_cx; JS::PersistentRootedObject m_glob; // global scope object public: boost::rand48* m_rng; JS::PersistentRootedObject m_nativeScope; // native function scope object }; ScriptInterface::Request::Request(const ScriptInterface& scriptInterface) : cx(scriptInterface.m->m_cx) { JS_BeginRequest(cx); m_formerCompartment = JS_EnterCompartment(cx, scriptInterface.m->m_glob); glob = JS::CurrentGlobalOrNull(cx); } JS::Value ScriptInterface::Request::globalValue() const { return JS::ObjectValue(*glob); } ScriptInterface::Request::~Request() { JS_LeaveCompartment(cx, m_formerCompartment); JS_EndRequest(cx); } namespace { JSClass global_class = { "global", JSCLASS_GLOBAL_FLAGS, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, JS_GlobalObjectTraceHook }; // Functions in the global namespace: bool print(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); ScriptInterface::Request rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ for (uint i = 0; i < args.length(); ++i) { std::wstring str; if (!ScriptInterface::FromJSVal(rq, args[i], str)) return false; debug_printf("%s", utf8_from_wstring(str).c_str()); } fflush(stdout); args.rval().setUndefined(); return true; } bool logmsg(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() < 1) { args.rval().setUndefined(); return true; } ScriptInterface::Request rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ std::wstring str; if (!ScriptInterface::FromJSVal(rq, args[0], str)) return false; LOGMESSAGE("%s", utf8_from_wstring(str)); args.rval().setUndefined(); return true; } bool warn(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() < 1) { args.rval().setUndefined(); return true; } ScriptInterface::Request rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ std::wstring str; if (!ScriptInterface::FromJSVal(rq, args[0], str)) return false; LOGWARNING("%s", utf8_from_wstring(str)); args.rval().setUndefined(); return true; } bool error(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() < 1) { args.rval().setUndefined(); return true; } ScriptInterface::Request rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ std::wstring str; if (!ScriptInterface::FromJSVal(rq, args[0], str)) return false; LOGERROR("%s", utf8_from_wstring(str)); args.rval().setUndefined(); return true; } bool deepcopy(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() < 1) { args.rval().setUndefined(); return true; } ScriptInterface::Request rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ JS::RootedValue ret(cx); if (!JS_StructuredClone(rq.cx, args[0], &ret, NULL, NULL)) return false; args.rval().set(ret); return true; } bool deepfreeze(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); ScriptInterface::Request rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ if (args.length() != 1 || !args.get(0).isObject()) { JS_ReportError(cx, "deepfreeze requires exactly one object as an argument."); return false; } ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface->FreezeObject(args.get(0), true); args.rval().set(args.get(0)); return true; } bool ProfileStart(JSContext* cx, uint argc, JS::Value* vp) { const char* name = "(ProfileStart)"; JS::CallArgs args = JS::CallArgsFromVp(argc, vp); ScriptInterface::Request rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ if (args.length() >= 1) { std::string str; if (!ScriptInterface::FromJSVal(rq, args[0], str)) return false; typedef boost::flyweight< std::string, boost::flyweights::no_tracking, boost::flyweights::no_locking > StringFlyweight; name = StringFlyweight(str).get().c_str(); } if (CProfileManager::IsInitialised() && ThreadUtil::IsMainThread()) g_Profiler.StartScript(name); g_Profiler2.RecordRegionEnter(name); args.rval().setUndefined(); return true; } bool ProfileStop(JSContext* UNUSED(cx), uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (CProfileManager::IsInitialised() && ThreadUtil::IsMainThread()) g_Profiler.Stop(); g_Profiler2.RecordRegionLeave(); args.rval().setUndefined(); return true; } bool ProfileAttribute(JSContext* cx, uint argc, JS::Value* vp) { const char* name = "(ProfileAttribute)"; JS::CallArgs args = JS::CallArgsFromVp(argc, vp); ScriptInterface::Request rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ if (args.length() >= 1) { std::string str; if (!ScriptInterface::FromJSVal(rq, args[0], str)) return false; typedef boost::flyweight< std::string, boost::flyweights::no_tracking, boost::flyweights::no_locking > StringFlyweight; name = StringFlyweight(str).get().c_str(); } g_Profiler2.RecordAttribute("%s", name); args.rval().setUndefined(); return true; } // Math override functions: // boost::uniform_real is apparently buggy in Boost pre-1.47 - for integer generators // it returns [min,max], not [min,max). The bug was fixed in 1.47. // We need consistent behaviour, so manually implement the correct version: static double generate_uniform_real(boost::rand48& rng, double min, double max) { while (true) { double n = (double)(rng() - rng.min()); double d = (double)(rng.max() - rng.min()) + 1.0; ENSURE(d > 0 && n >= 0 && n <= d); double r = n / d * (max - min) + min; if (r < max) return r; } } bool Math_random(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); double r; if (!ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface->MathRandom(r)) return false; args.rval().setNumber(r); return true; } } // anonymous namespace bool ScriptInterface::MathRandom(double& nbr) { if (m->m_rng == NULL) return false; nbr = generate_uniform_real(*(m->m_rng), 0.0, 1.0); return true; } -ScriptInterface_impl::ScriptInterface_impl(const char* nativeScopeName, const shared_ptr& runtime) : - m_runtime(runtime), m_cx(runtime->GetGeneralJSContext()), m_glob(runtime->GetJSRuntime()), m_nativeScope(runtime->GetJSRuntime()) +ScriptInterface_impl::ScriptInterface_impl(const char* nativeScopeName, const shared_ptr& context) : + m_context(context), m_cx(context->GetGeneralJSContext()), m_glob(context->GetJSRuntime()), m_nativeScope(context->GetJSRuntime()) { JS::CompartmentOptions opt; opt.setVersion(JSVERSION_LATEST); // Keep JIT code during non-shrinking GCs. This brings a quite big performance improvement. opt.setPreserveJitCode(true); JSAutoRequest rq(m_cx); m_glob = JS_NewGlobalObject(m_cx, &global_class, nullptr, JS::OnNewGlobalHookOption::FireOnNewGlobalHook, opt); JSAutoCompartment autoCmpt(m_cx, m_glob); ENSURE(JS_InitStandardClasses(m_cx, m_glob)); JS_DefineProperty(m_cx, m_glob, "global", m_glob, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); m_nativeScope = JS_DefineObject(m_cx, m_glob, nativeScopeName, nullptr, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "print", ::print, 0, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "log", ::logmsg, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "warn", ::warn, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "error", ::error, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "clone", ::deepcopy, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "deepfreeze", ::deepfreeze, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); Register("ProfileStart", ::ProfileStart, 1); Register("ProfileStop", ::ProfileStop, 0); Register("ProfileAttribute", ::ProfileAttribute, 1); - m_runtime->RegisterCompartment(js::GetObjectCompartment(m_glob)); + m_context->RegisterCompartment(js::GetObjectCompartment(m_glob)); } ScriptInterface_impl::~ScriptInterface_impl() { - m_runtime->UnRegisterCompartment(js::GetObjectCompartment(m_glob)); + m_context->UnRegisterCompartment(js::GetObjectCompartment(m_glob)); } void ScriptInterface_impl::Register(const char* name, JSNative fptr, uint nargs) const { JSAutoRequest rq(m_cx); JSAutoCompartment autoCmpt(m_cx, m_glob); JS::RootedObject nativeScope(m_cx, m_nativeScope); JS::RootedFunction func(m_cx, JS_DefineFunction(m_cx, nativeScope, name, fptr, nargs, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT)); } -ScriptInterface::ScriptInterface(const char* nativeScopeName, const char* debugName, const shared_ptr& runtime) : - m(new ScriptInterface_impl(nativeScopeName, runtime)) +ScriptInterface::ScriptInterface(const char* nativeScopeName, const char* debugName, const shared_ptr& context) : + m(new ScriptInterface_impl(nativeScopeName, context)) { // Profiler stats table isn't thread-safe, so only enable this on the main thread if (ThreadUtil::IsMainThread()) { if (g_ScriptStatsTable) g_ScriptStatsTable->Add(this, debugName); } Request rq(this); m_CmptPrivate.pScriptInterface = this; JS_SetCompartmentPrivate(js::GetObjectCompartment(rq.glob), (void*)&m_CmptPrivate); } ScriptInterface::~ScriptInterface() { if (ThreadUtil::IsMainThread()) { if (g_ScriptStatsTable) g_ScriptStatsTable->Remove(this); } } void ScriptInterface::SetCallbackData(void* pCBData) { m_CmptPrivate.pCBData = pCBData; } ScriptInterface::CmptPrivate* ScriptInterface::GetScriptInterfaceAndCBData(JSContext* cx) { CmptPrivate* pCmptPrivate = (CmptPrivate*)JS_GetCompartmentPrivate(js::GetContextCompartment(cx)); return pCmptPrivate; } bool ScriptInterface::LoadGlobalScripts() { // Ignore this failure in tests if (!g_VFS) return false; // Load and execute *.js in the global scripts directory VfsPaths pathnames; vfs::GetPathnames(g_VFS, L"globalscripts/", L"*.js", pathnames); for (const VfsPath& path : pathnames) if (!LoadGlobalScriptFile(path)) { LOGERROR("LoadGlobalScripts: Failed to load script %s", path.string8()); return false; } return true; } bool ScriptInterface::ReplaceNondeterministicRNG(boost::rand48& rng) { Request rq(this); JS::RootedValue math(rq.cx); JS::RootedObject global(rq.cx, rq.glob); if (JS_GetProperty(rq.cx, global, "Math", &math) && math.isObject()) { JS::RootedObject mathObj(rq.cx, &math.toObject()); JS::RootedFunction random(rq.cx, JS_DefineFunction(rq.cx, mathObj, "random", Math_random, 0, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT)); if (random) { m->m_rng = &rng; return true; } } LOGERROR("ReplaceNondeterministicRNG: failed to replace Math.random"); return false; } void ScriptInterface::Register(const char* name, JSNative fptr, size_t nargs) const { m->Register(name, fptr, (uint)nargs); } JSRuntime* ScriptInterface::GetJSRuntime() const { - return m->m_runtime->GetJSRuntime(); + return m->m_context->GetJSRuntime(); } -shared_ptr ScriptInterface::GetRuntime() const +shared_ptr ScriptInterface::GetContext() const { - return m->m_runtime; + return m->m_context; } void ScriptInterface::CallConstructor(JS::HandleValue ctor, JS::HandleValueArray argv, JS::MutableHandleValue out) const { Request rq(this); if (!ctor.isObject()) { LOGERROR("CallConstructor: ctor is not an object"); out.setNull(); return; } JS::RootedObject ctorObj(rq.cx, &ctor.toObject()); out.setObjectOrNull(JS_New(rq.cx, ctorObj, argv)); } void ScriptInterface::DefineCustomObjectType(JSClass *clasp, JSNative constructor, uint minArgs, JSPropertySpec *ps, JSFunctionSpec *fs, JSPropertySpec *static_ps, JSFunctionSpec *static_fs) { Request rq(this); std::string typeName = clasp->name; if (m_CustomObjectTypes.find(typeName) != m_CustomObjectTypes.end()) { // This type already exists throw PSERROR_Scripting_DefineType_AlreadyExists(); } JS::RootedObject global(rq.cx, rq.glob); JS::RootedObject obj(rq.cx, JS_InitClass(rq.cx, global, nullptr, clasp, constructor, minArgs, // Constructor, min args ps, fs, // Properties, methods static_ps, static_fs)); // Constructor properties, methods if (obj == NULL) throw PSERROR_Scripting_DefineType_CreationFailed(); CustomType& type = m_CustomObjectTypes[typeName]; type.m_Prototype.init(rq.cx, obj); type.m_Class = clasp; type.m_Constructor = constructor; } JSObject* ScriptInterface::CreateCustomObject(const std::string& typeName) const { std::map::const_iterator it = m_CustomObjectTypes.find(typeName); if (it == m_CustomObjectTypes.end()) throw PSERROR_Scripting_TypeDoesNotExist(); Request rq(this); JS::RootedObject prototype(rq.cx, it->second.m_Prototype.get()); return JS_NewObjectWithGivenProto(rq.cx, it->second.m_Class, prototype); } bool ScriptInterface::CallFunction_(JS::HandleValue val, const char* name, JS::HandleValueArray argv, JS::MutableHandleValue ret) const { Request rq(this); JS::RootedObject obj(rq.cx); if (!JS_ValueToObject(rq.cx, val, &obj) || !obj) return false; // Check that the named function actually exists, to avoid ugly JS error reports // when calling an undefined value bool found; if (!JS_HasProperty(rq.cx, obj, name, &found) || !found) return false; return JS_CallFunctionName(rq.cx, obj, name, argv, ret); } bool ScriptInterface::CreateObject_(const Request& rq, JS::MutableHandleObject object) { object.set(JS_NewPlainObject(rq.cx)); if (!object) throw PSERROR_Scripting_CreateObjectFailed(); return true; } void ScriptInterface::CreateArray(const Request& rq, JS::MutableHandleValue objectValue, size_t length) { objectValue.setObjectOrNull(JS_NewArrayObject(rq.cx, length)); if (!objectValue.isObject()) throw PSERROR_Scripting_CreateObjectFailed(); } bool ScriptInterface::SetGlobal_(const char* name, JS::HandleValue value, bool replace, bool constant, bool enumerate) { Request rq(this); JS::RootedObject global(rq.cx, rq.glob); bool found; if (!JS_HasProperty(rq.cx, global, name, &found)) return false; if (found) { JS::Rooted desc(rq.cx); if (!JS_GetOwnPropertyDescriptor(rq.cx, global, name, &desc)) return false; if (!desc.writable()) { if (!replace) { JS_ReportError(rq.cx, "SetGlobal \"%s\" called multiple times", name); return false; } // This is not supposed to happen, unless the user has called SetProperty with constant = true on the global object // instead of using SetGlobal. if (!desc.configurable()) { JS_ReportError(rq.cx, "The global \"%s\" is permanent and cannot be hotloaded", name); return false; } LOGMESSAGE("Hotloading new value for global \"%s\".", name); ENSURE(JS_DeleteProperty(rq.cx, global, name)); } } uint attrs = 0; if (constant) attrs |= JSPROP_READONLY; if (enumerate) attrs |= JSPROP_ENUMERATE; return JS_DefineProperty(rq.cx, global, name, value, attrs); } bool ScriptInterface::SetProperty_(JS::HandleValue obj, const char* name, JS::HandleValue value, bool constant, bool enumerate) const { Request rq(this); uint attrs = 0; if (constant) attrs |= JSPROP_READONLY | JSPROP_PERMANENT; if (enumerate) attrs |= JSPROP_ENUMERATE; if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); if (!JS_DefineProperty(rq.cx, object, name, value, attrs)) return false; return true; } bool ScriptInterface::SetProperty_(JS::HandleValue obj, const wchar_t* name, JS::HandleValue value, bool constant, bool enumerate) const { Request rq(this); uint attrs = 0; if (constant) attrs |= JSPROP_READONLY | JSPROP_PERMANENT; if (enumerate) attrs |= JSPROP_ENUMERATE; if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); utf16string name16(name, name + wcslen(name)); if (!JS_DefineUCProperty(rq.cx, object, reinterpret_cast(name16.c_str()), name16.length(), value, attrs)) return false; return true; } bool ScriptInterface::SetPropertyInt_(JS::HandleValue obj, int name, JS::HandleValue value, bool constant, bool enumerate) const { Request rq(this); uint attrs = 0; if (constant) attrs |= JSPROP_READONLY | JSPROP_PERMANENT; if (enumerate) attrs |= JSPROP_ENUMERATE; if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); JS::RootedId id(rq.cx, INT_TO_JSID(name)); if (!JS_DefinePropertyById(rq.cx, object, id, value, attrs)) return false; return true; } bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const { return GetProperty_(obj, name, out); } bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleObject out) const { Request rq(this); JS::RootedValue val(rq.cx); if (!GetProperty_(obj, name, &val)) return false; if (!val.isObject()) { LOGERROR("GetProperty failed: trying to get an object, but the property is not an object!"); return false; } out.set(&val.toObject()); return true; } bool ScriptInterface::GetPropertyInt(JS::HandleValue obj, int name, JS::MutableHandleValue out) const { return GetPropertyInt_(obj, name, out); } bool ScriptInterface::GetProperty_(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const { Request rq(this); if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); if (!JS_GetProperty(rq.cx, object, name, out)) return false; return true; } bool ScriptInterface::GetPropertyInt_(JS::HandleValue obj, int name, JS::MutableHandleValue out) const { Request rq(this); JS::RootedId nameId(rq.cx, INT_TO_JSID(name)); if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); if (!JS_GetPropertyById(rq.cx, object, nameId, out)) return false; return true; } bool ScriptInterface::HasProperty(JS::HandleValue obj, const char* name) const { // TODO: proper errorhandling Request rq(this); if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); bool found; if (!JS_HasProperty(rq.cx, object, name, &found)) return false; return found; } bool ScriptInterface::EnumeratePropertyNames(JS::HandleValue objVal, bool enumerableOnly, std::vector& out) const { Request rq(this); if (!objVal.isObjectOrNull()) { LOGERROR("EnumeratePropertyNames expected object type!"); return false; } JS::RootedObject obj(rq.cx, &objVal.toObject()); JS::AutoIdVector props(rq.cx); // This recurses up the prototype chain on its own. if (!js::GetPropertyKeys(rq.cx, obj, enumerableOnly? 0 : JSITER_HIDDEN, &props)) return false; out.reserve(out.size() + props.length()); for (size_t i = 0; i < props.length(); ++i) { JS::RootedId id(rq.cx, props[i]); JS::RootedValue val(rq.cx); if (!JS_IdToValue(rq.cx, id, &val)) return false; // Ignore integer properties for now. // TODO: is this actually a thing in ECMAScript 6? if (!val.isString()) continue; std::string propName; if (!FromJSVal(rq, val, propName)) return false; out.emplace_back(std::move(propName)); } return true; } bool ScriptInterface::SetPrototype(JS::HandleValue objVal, JS::HandleValue protoVal) { Request rq(this); if (!objVal.isObject() || !protoVal.isObject()) return false; JS::RootedObject obj(rq.cx, &objVal.toObject()); JS::RootedObject proto(rq.cx, &protoVal.toObject()); return JS_SetPrototype(rq.cx, obj, proto); } bool ScriptInterface::FreezeObject(JS::HandleValue objVal, bool deep) const { Request rq(this); if (!objVal.isObject()) return false; JS::RootedObject obj(rq.cx, &objVal.toObject()); if (deep) return JS_DeepFreezeObject(rq.cx, obj); else return JS_FreezeObject(rq.cx, obj); } bool ScriptInterface::LoadScript(const VfsPath& filename, const std::string& code) const { Request rq(this); JS::RootedObject global(rq.cx, rq.glob); utf16string codeUtf16(code.begin(), code.end()); uint lineNo = 1; // CompileOptions does not copy the contents of the filename string pointer. // Passing a temporary string there will cause undefined behaviour, so we create a separate string to avoid the temporary. std::string filenameStr = filename.string8(); JS::CompileOptions options(rq.cx); options.setFileAndLine(filenameStr.c_str(), lineNo); options.setIsRunOnce(false); JS::RootedFunction func(rq.cx); JS::AutoObjectVector emptyScopeChain(rq.cx); if (!JS::CompileFunction(rq.cx, emptyScopeChain, options, NULL, 0, NULL, reinterpret_cast(codeUtf16.c_str()), (uint)(codeUtf16.length()), &func)) return false; JS::RootedValue rval(rq.cx); return JS_CallFunction(rq.cx, nullptr, func, JS::HandleValueArray::empty(), &rval); } bool ScriptInterface::LoadGlobalScript(const VfsPath& filename, const std::wstring& code) const { Request rq(this); utf16string codeUtf16(code.begin(), code.end()); uint lineNo = 1; // CompileOptions does not copy the contents of the filename string pointer. // Passing a temporary string there will cause undefined behaviour, so we create a separate string to avoid the temporary. std::string filenameStr = filename.string8(); JS::RootedValue rval(rq.cx); JS::CompileOptions opts(rq.cx); opts.setFileAndLine(filenameStr.c_str(), lineNo); return JS::Evaluate(rq.cx, opts, reinterpret_cast(codeUtf16.c_str()), (uint)(codeUtf16.length()), &rval); } bool ScriptInterface::LoadGlobalScriptFile(const VfsPath& path) const { Request rq(this); if (!VfsFileExists(path)) { LOGERROR("File '%s' does not exist", path.string8()); return false; } CVFSFile file; PSRETURN ret = file.Load(g_VFS, path); if (ret != PSRETURN_OK) { LOGERROR("Failed to load file '%s': %s", path.string8(), GetErrorString(ret)); return false; } std::wstring code = wstring_from_utf8(file.DecodeUTF8()); // assume it's UTF-8 utf16string codeUtf16(code.begin(), code.end()); uint lineNo = 1; // CompileOptions does not copy the contents of the filename string pointer. // Passing a temporary string there will cause undefined behaviour, so we create a separate string to avoid the temporary. std::string filenameStr = path.string8(); JS::RootedValue rval(rq.cx); JS::CompileOptions opts(rq.cx); opts.setFileAndLine(filenameStr.c_str(), lineNo); return JS::Evaluate(rq.cx, opts, reinterpret_cast(codeUtf16.c_str()), (uint)(codeUtf16.length()), &rval); } bool ScriptInterface::Eval(const char* code) const { Request rq(this); JS::RootedValue rval(rq.cx); return Eval_(code, &rval); } bool ScriptInterface::Eval_(const char* code, JS::MutableHandleValue rval) const { Request rq(this); utf16string codeUtf16(code, code+strlen(code)); JS::CompileOptions opts(rq.cx); opts.setFileAndLine("(eval)", 1); return JS::Evaluate(rq.cx, opts, reinterpret_cast(codeUtf16.c_str()), (uint)codeUtf16.length(), rval); } bool ScriptInterface::Eval_(const wchar_t* code, JS::MutableHandleValue rval) const { Request rq(this); utf16string codeUtf16(code, code+wcslen(code)); JS::CompileOptions opts(rq.cx); opts.setFileAndLine("(eval)", 1); return JS::Evaluate(rq.cx, opts, reinterpret_cast(codeUtf16.c_str()), (uint)codeUtf16.length(), rval); } bool ScriptInterface::ParseJSON(const std::string& string_utf8, JS::MutableHandleValue out) const { Request rq(this); std::wstring attrsW = wstring_from_utf8(string_utf8); utf16string string(attrsW.begin(), attrsW.end()); if (JS_ParseJSON(rq.cx, reinterpret_cast(string.c_str()), (u32)string.size(), out)) return true; LOGERROR("JS_ParseJSON failed!"); if (!JS_IsExceptionPending(rq.cx)) return false; JS::RootedValue exc(rq.cx); if (!JS_GetPendingException(rq.cx, &exc)) return false; JS_ClearPendingException(rq.cx); // We expect an object of type SyntaxError if (!exc.isObject()) return false; JS::RootedValue rval(rq.cx); JS::RootedObject excObj(rq.cx, &exc.toObject()); if (!JS_CallFunctionName(rq.cx, excObj, "toString", JS::HandleValueArray::empty(), &rval)) return false; std::wstring error; ScriptInterface::FromJSVal(rq, rval, error); LOGERROR("%s", utf8_from_wstring(error)); return false; } void ScriptInterface::ReadJSONFile(const VfsPath& path, JS::MutableHandleValue out) const { if (!VfsFileExists(path)) { LOGERROR("File '%s' does not exist", path.string8()); return; } CVFSFile file; PSRETURN ret = file.Load(g_VFS, path); if (ret != PSRETURN_OK) { LOGERROR("Failed to load file '%s': %s", path.string8(), GetErrorString(ret)); return; } std::string content(file.DecodeUTF8()); // assume it's UTF-8 if (!ParseJSON(content, out)) LOGERROR("Failed to parse '%s'", path.string8()); } struct Stringifier { static bool callback(const char16_t* buf, u32 len, void* data) { utf16string str(buf, buf+len); std::wstring strw(str.begin(), str.end()); Status err; // ignore Unicode errors static_cast(data)->stream << utf8_from_wstring(strw, &err); return true; } std::stringstream stream; }; // TODO: It's not quite clear why JS_Stringify needs JS::MutableHandleValue. |obj| should not get modified. // It probably has historical reasons and could be changed by SpiderMonkey in the future. std::string ScriptInterface::StringifyJSON(JS::MutableHandleValue obj, bool indent) const { Request rq(this); Stringifier str; JS::RootedValue indentVal(rq.cx, indent ? JS::Int32Value(2) : JS::UndefinedValue()); if (!JS_Stringify(rq.cx, obj, nullptr, indentVal, &Stringifier::callback, &str)) { JS_ClearPendingException(rq.cx); LOGERROR("StringifyJSON failed"); return std::string(); } return str.stream.str(); } std::string ScriptInterface::ToString(JS::MutableHandleValue obj, bool pretty) const { Request rq(this); if (obj.isUndefined()) return "(void 0)"; // Try to stringify as JSON if possible // (TODO: this is maybe a bad idea since it'll drop 'undefined' values silently) if (pretty) { Stringifier str; JS::RootedValue indentVal(rq.cx, JS::Int32Value(2)); // Temporary disable the error reporter, so we don't print complaints about cyclic values JSErrorReporter er = JS_SetErrorReporter(GetJSRuntime(), nullptr); bool ok = JS_Stringify(rq.cx, obj, nullptr, indentVal, &Stringifier::callback, &str); // Restore error reporter JS_SetErrorReporter(GetJSRuntime(), er); if (ok) return str.stream.str(); // Clear the exception set when Stringify failed JS_ClearPendingException(rq.cx); } // Caller didn't want pretty output, or JSON conversion failed (e.g. due to cycles), // so fall back to obj.toSource() std::wstring source = L"(error)"; CallFunction(obj, "toSource", source); return utf8_from_wstring(source); } void ScriptInterface::ReportError(const char* msg) const { Request rq(this); // JS_ReportError by itself doesn't seem to set a JS-style exception, and so // script callers will be unable to catch anything. So use JS_SetPendingException // to make sure there really is a script-level exception. But just set it to undefined // because there's not much value yet in throwing a real exception object. JS_SetPendingException(rq.cx, JS::UndefinedHandleValue); // And report the actual error JS_ReportError(rq.cx, "%s", msg); // TODO: Why doesn't JS_ReportPendingException(cx); work? } bool ScriptInterface::IsExceptionPending(const Request& rq) { return JS_IsExceptionPending(rq.cx) ? true : false; } JS::Value ScriptInterface::CloneValueFromOtherCompartment(const ScriptInterface& otherCompartment, JS::HandleValue val) const { PROFILE("CloneValueFromOtherCompartment"); Request rq(this); JS::RootedValue out(rq.cx); shared_ptr structuredClone = otherCompartment.WriteStructuredClone(val); ReadStructuredClone(structuredClone, &out); return out.get(); } ScriptInterface::StructuredClone::StructuredClone() : m_Data(NULL), m_Size(0) { } ScriptInterface::StructuredClone::~StructuredClone() { if (m_Data) JS_ClearStructuredClone(m_Data, m_Size, NULL, NULL); } shared_ptr ScriptInterface::WriteStructuredClone(JS::HandleValue v) const { Request rq(this); u64* data = NULL; size_t nbytes = 0; if (!JS_WriteStructuredClone(rq.cx, v, &data, &nbytes, NULL, NULL, JS::UndefinedHandleValue)) { debug_warn(L"Writing a structured clone with JS_WriteStructuredClone failed!"); return shared_ptr(); } shared_ptr ret(new StructuredClone); ret->m_Data = data; ret->m_Size = nbytes; return ret; } void ScriptInterface::ReadStructuredClone(const shared_ptr& ptr, JS::MutableHandleValue ret) const { Request rq(this); JS_ReadStructuredClone(rq.cx, ptr->m_Data, ptr->m_Size, JS_STRUCTURED_CLONE_VERSION, ret, NULL, NULL); } Index: ps/trunk/source/scriptinterface/tests/test_ScriptConversions.h =================================================================== --- ps/trunk/source/scriptinterface/tests/test_ScriptConversions.h (revision 24180) +++ ps/trunk/source/scriptinterface/tests/test_ScriptConversions.h (revision 24181) @@ -1,272 +1,272 @@ /* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "lib/self_test.h" #include "scriptinterface/ScriptInterface.h" #include "maths/Fixed.h" #include "maths/FixedVector2D.h" #include "maths/FixedVector3D.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "jsapi.h" class TestScriptConversions : public CxxTest::TestSuite { template void convert_to(const T& value, const std::string& expected) { - ScriptInterface script("Test", "Test", g_ScriptRuntime); + ScriptInterface script("Test", "Test", g_ScriptContext); TS_ASSERT(script.LoadGlobalScripts()); ScriptInterface::Request 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_ScriptRuntime); + ScriptInterface script("Test", "Test", g_ScriptContext); TS_ASSERT(script.LoadGlobalScripts()); ScriptInterface::Request 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_ScriptRuntime); + ScriptInterface script("Test", "Test", g_ScriptContext); TS_ASSERT(script.LoadGlobalScripts()); ScriptInterface::Request 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)); } 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_ScriptRuntime); + ScriptInterface script("Test", "Test", g_ScriptContext); ScriptInterface::Request 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_ScriptRuntime); + ScriptInterface script("Test", "Test", g_ScriptContext); ScriptInterface::Request 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_ScriptRuntime); + ScriptInterface script("Test", "Test", g_ScriptContext); TS_ASSERT(script.LoadGlobalScripts()); ScriptInterface::Request 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/Simulation2.cpp =================================================================== --- ps/trunk/source/simulation2/Simulation2.cpp (revision 24180) +++ ps/trunk/source/simulation2/Simulation2.cpp (revision 24181) @@ -1,995 +1,995 @@ /* Copyright (C) 2020 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 "Simulation2.h" +#include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptInterface.h" -#include "scriptinterface/ScriptRuntime.h" #include "simulation2/MessageTypes.h" #include "simulation2/system/ComponentManager.h" #include "simulation2/system/ParamNode.h" #include "simulation2/system/SimContext.h" #include "simulation2/components/ICmpAIManager.h" #include "simulation2/components/ICmpCommandQueue.h" #include "simulation2/components/ICmpTemplateManager.h" #include "graphics/MapReader.h" #include "graphics/Terrain.h" #include "lib/timer.h" #include "lib/file/vfs/vfs_util.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Loader.h" #include "ps/Profile.h" #include "ps/Pyrogenesis.h" #include "ps/Util.h" #include "ps/XML/Xeromyces.h" #include class CSimulation2Impl { public: - CSimulation2Impl(CUnitManager* unitManager, shared_ptr rt, CTerrain* terrain) : + CSimulation2Impl(CUnitManager* unitManager, shared_ptr rt, CTerrain* terrain) : m_SimContext(), m_ComponentManager(m_SimContext, rt), m_EnableOOSLog(false), m_EnableSerializationTest(false), m_RejoinTestTurn(-1), m_TestingRejoin(false), m_SecondaryTerrain(nullptr), m_SecondaryContext(nullptr), m_SecondaryComponentManager(nullptr), m_SecondaryLoadedScripts(nullptr), m_MapSettings(rt->GetJSRuntime()), m_InitAttributes(rt->GetJSRuntime()) { m_SimContext.m_UnitManager = unitManager; m_SimContext.m_Terrain = terrain; m_ComponentManager.LoadComponentTypes(); RegisterFileReloadFunc(ReloadChangedFileCB, this); // Tests won't have config initialised if (CConfigDB::IsInitialised()) { CFG_GET_VAL("ooslog", m_EnableOOSLog); CFG_GET_VAL("serializationtest", m_EnableSerializationTest); CFG_GET_VAL("rejointest", m_RejoinTestTurn); if (m_RejoinTestTurn < 0) // Handle bogus values of the arg m_RejoinTestTurn = -1; } if (m_EnableOOSLog) { m_OOSLogPath = createDateIndexSubdirectory(psLogDir() / "oos_logs"); debug_printf("Writing ooslogs to %s\n", m_OOSLogPath.string8().c_str()); } } ~CSimulation2Impl() { delete m_SecondaryTerrain; delete m_SecondaryContext; delete m_SecondaryComponentManager; delete m_SecondaryLoadedScripts; UnregisterFileReloadFunc(ReloadChangedFileCB, this); } void ResetState(bool skipScriptedComponents, bool skipAI) { m_DeltaTime = 0.0; m_LastFrameOffset = 0.0f; m_TurnNumber = 0; ResetComponentState(m_ComponentManager, skipScriptedComponents, skipAI); } static void ResetComponentState(CComponentManager& componentManager, bool skipScriptedComponents, bool skipAI) { componentManager.ResetState(); componentManager.InitSystemEntity(); componentManager.AddSystemComponents(skipScriptedComponents, skipAI); } static bool LoadDefaultScripts(CComponentManager& componentManager, std::set* loadedScripts); static bool LoadScripts(CComponentManager& componentManager, std::set* loadedScripts, const VfsPath& path); static bool LoadTriggerScripts(CComponentManager& componentManager, JS::HandleValue mapSettings, std::set* loadedScripts); Status ReloadChangedFile(const VfsPath& path); static Status ReloadChangedFileCB(void* param, const VfsPath& path) { return static_cast(param)->ReloadChangedFile(path); } int ProgressiveLoad(); void Update(int turnLength, const std::vector& commands); static void UpdateComponents(CSimContext& simContext, fixed turnLengthFixed, const std::vector& commands); void Interpolate(float simFrameLength, float frameOffset, float realFrameLength); void DumpState(); CSimContext m_SimContext; CComponentManager m_ComponentManager; double m_DeltaTime; float m_LastFrameOffset; std::string m_StartupScript; JS::PersistentRootedValue m_InitAttributes; JS::PersistentRootedValue m_MapSettings; std::set m_LoadedScripts; uint32_t m_TurnNumber; bool m_EnableOOSLog; OsPath m_OOSLogPath; // Functions and data for the serialization test mode: (see Update() for relevant comments) bool m_EnableSerializationTest; int m_RejoinTestTurn; bool m_TestingRejoin; // Secondary simulation CTerrain* m_SecondaryTerrain; CSimContext* m_SecondaryContext; CComponentManager* m_SecondaryComponentManager; std::set* m_SecondaryLoadedScripts; struct SerializationTestState { std::stringstream state; std::stringstream debug; std::string hash; }; void DumpSerializationTestState(SerializationTestState& state, const OsPath& path, const OsPath::String& suffix); void ReportSerializationFailure( SerializationTestState* primaryStateBefore, SerializationTestState* primaryStateAfter, SerializationTestState* secondaryStateBefore, SerializationTestState* secondaryStateAfter); void InitRNGSeedSimulation(); void InitRNGSeedAI(); static std::vector CloneCommandsFromOtherContext(const ScriptInterface& oldScript, const ScriptInterface& newScript, const std::vector& commands) { std::vector newCommands; newCommands.reserve(commands.size()); ScriptInterface::Request rqNew(newScript); for (const SimulationCommand& command : commands) { JS::RootedValue tmpCommand(rqNew.cx, newScript.CloneValueFromOtherCompartment(oldScript, command.data)); newScript.FreezeObject(tmpCommand, true); SimulationCommand cmd(command.player, rqNew.cx, tmpCommand); newCommands.emplace_back(std::move(cmd)); } return newCommands; } }; bool CSimulation2Impl::LoadDefaultScripts(CComponentManager& componentManager, std::set* loadedScripts) { return ( LoadScripts(componentManager, loadedScripts, L"simulation/components/interfaces/") && LoadScripts(componentManager, loadedScripts, L"simulation/helpers/") && LoadScripts(componentManager, loadedScripts, L"simulation/components/") ); } bool CSimulation2Impl::LoadScripts(CComponentManager& componentManager, std::set* loadedScripts, const VfsPath& path) { VfsPaths pathnames; if (vfs::GetPathnames(g_VFS, path, L"*.js", pathnames) < 0) return false; bool ok = true; for (const VfsPath& path : pathnames) { if (loadedScripts) loadedScripts->insert(path); LOGMESSAGE("Loading simulation script '%s'", path.string8()); if (!componentManager.LoadScript(path)) ok = false; } return ok; } bool CSimulation2Impl::LoadTriggerScripts(CComponentManager& componentManager, JS::HandleValue mapSettings, std::set* loadedScripts) { bool ok = true; if (componentManager.GetScriptInterface().HasProperty(mapSettings, "TriggerScripts")) { std::vector scriptNames; componentManager.GetScriptInterface().GetProperty(mapSettings, "TriggerScripts", scriptNames); for (const std::string& triggerScript : scriptNames) { std::string scriptName = "maps/" + triggerScript; if (loadedScripts) { if (loadedScripts->find(scriptName) != loadedScripts->end()) continue; loadedScripts->insert(scriptName); } LOGMESSAGE("Loading trigger script '%s'", scriptName.c_str()); if (!componentManager.LoadScript(scriptName.data())) ok = false; } } return ok; } Status CSimulation2Impl::ReloadChangedFile(const VfsPath& path) { // Ignore if this file wasn't loaded as a script // (TODO: Maybe we ought to load in any new .js files that are created in the right directories) if (m_LoadedScripts.find(path) == m_LoadedScripts.end()) return INFO::OK; // If the file doesn't exist (e.g. it was deleted), don't bother loading it since that'll give an error message. // (Also don't bother trying to 'unload' it from the component manager, because that's not possible) if (!VfsFileExists(path)) return INFO::OK; LOGMESSAGE("Reloading simulation script '%s'", path.string8()); if (!m_ComponentManager.LoadScript(path, true)) return ERR::FAIL; return INFO::OK; } int CSimulation2Impl::ProgressiveLoad() { // yield after this time is reached. balances increased progress bar // smoothness vs. slowing down loading. const double end_time = timer_Time() + 200e-3; int ret; do { bool progressed = false; int total = 0; int progress = 0; CMessageProgressiveLoad msg(&progressed, &total, &progress); m_ComponentManager.BroadcastMessage(msg); if (!progressed || total == 0) return 0; // we have nothing left to load ret = Clamp(100*progress / total, 1, 100); } while (timer_Time() < end_time); return ret; } void CSimulation2Impl::DumpSerializationTestState(SerializationTestState& state, const OsPath& path, const OsPath::String& suffix) { if (!state.hash.empty()) { std::ofstream file (OsString(path / (L"hash." + suffix)).c_str(), std::ofstream::out | std::ofstream::trunc); file << Hexify(state.hash); } if (!state.debug.str().empty()) { std::ofstream file (OsString(path / (L"debug." + suffix)).c_str(), std::ofstream::out | std::ofstream::trunc); file << state.debug.str(); } if (!state.state.str().empty()) { std::ofstream file (OsString(path / (L"state." + suffix)).c_str(), std::ofstream::out | std::ofstream::trunc | std::ofstream::binary); file << state.state.str(); } } void CSimulation2Impl::ReportSerializationFailure( SerializationTestState* primaryStateBefore, SerializationTestState* primaryStateAfter, SerializationTestState* secondaryStateBefore, SerializationTestState* secondaryStateAfter) { const OsPath path = createDateIndexSubdirectory(psLogDir() / "serializationtest"); debug_printf("Writing serializationtest-data to %s\n", path.string8().c_str()); // Clean up obsolete files from previous runs wunlink(path / "hash.before.a"); wunlink(path / "hash.before.b"); wunlink(path / "debug.before.a"); wunlink(path / "debug.before.b"); wunlink(path / "state.before.a"); wunlink(path / "state.before.b"); wunlink(path / "hash.after.a"); wunlink(path / "hash.after.b"); wunlink(path / "debug.after.a"); wunlink(path / "debug.after.b"); wunlink(path / "state.after.a"); wunlink(path / "state.after.b"); if (primaryStateBefore) DumpSerializationTestState(*primaryStateBefore, path, L"before.a"); if (primaryStateAfter) DumpSerializationTestState(*primaryStateAfter, path, L"after.a"); if (secondaryStateBefore) DumpSerializationTestState(*secondaryStateBefore, path, L"before.b"); if (secondaryStateAfter) DumpSerializationTestState(*secondaryStateAfter, path, L"after.b"); debug_warn(L"Serialization test failure"); } void CSimulation2Impl::InitRNGSeedSimulation() { u32 seed = 0; if (!m_ComponentManager.GetScriptInterface().HasProperty(m_MapSettings, "Seed") || !m_ComponentManager.GetScriptInterface().GetProperty(m_MapSettings, "Seed", seed)) LOGWARNING("CSimulation2Impl::InitRNGSeedSimulation: No seed value specified - using %d", seed); m_ComponentManager.SetRNGSeed(seed); } void CSimulation2Impl::InitRNGSeedAI() { u32 seed = 0; if (!m_ComponentManager.GetScriptInterface().HasProperty(m_MapSettings, "AISeed") || !m_ComponentManager.GetScriptInterface().GetProperty(m_MapSettings, "AISeed", seed)) LOGWARNING("CSimulation2Impl::InitRNGSeedAI: No seed value specified - using %d", seed); CmpPtr cmpAIManager(m_SimContext, SYSTEM_ENTITY); if (cmpAIManager) cmpAIManager->SetRNGSeed(seed); } void CSimulation2Impl::Update(int turnLength, const std::vector& commands) { PROFILE3("sim update"); PROFILE2_ATTR("turn %d", (int)m_TurnNumber); fixed turnLengthFixed = fixed::FromInt(turnLength) / 1000; /* * In serialization test mode, we save the original (primary) simulation state before each turn update. * We run the update, then load the saved state into a secondary context. * We serialize that again and compare to the original serialization (to check that * serialize->deserialize->serialize is equivalent to serialize). * Then we run the update on the secondary context, and check that its new serialized * state matches the primary context after the update (to check that the simulation doesn't depend * on anything that's not serialized). * * In rejoin test mode, the secondary simulation is initialized from serialized data at turn N, then both * simulations run independantly while comparing their states each turn. This is way faster than a * complete serialization test and allows us to reproduce OOSes on rejoin. */ const bool serializationTestDebugDump = false; // set true to save human-readable state dumps before an error is detected, for debugging (but slow) const bool serializationTestHash = true; // set true to save and compare hash of state SerializationTestState primaryStateBefore; const ScriptInterface& scriptInterface = m_ComponentManager.GetScriptInterface(); const bool startRejoinTest = (int64_t) m_RejoinTestTurn == m_TurnNumber; if (startRejoinTest) m_TestingRejoin = true; if (m_EnableSerializationTest || m_TestingRejoin) { ENSURE(m_ComponentManager.SerializeState(primaryStateBefore.state)); if (serializationTestDebugDump) ENSURE(m_ComponentManager.DumpDebugState(primaryStateBefore.debug, false)); if (serializationTestHash) ENSURE(m_ComponentManager.ComputeStateHash(primaryStateBefore.hash, false)); } UpdateComponents(m_SimContext, turnLengthFixed, commands); if (m_EnableSerializationTest || startRejoinTest) { if (startRejoinTest) debug_printf("Initializing the secondary simulation\n"); delete m_SecondaryTerrain; m_SecondaryTerrain = new CTerrain(); delete m_SecondaryContext; m_SecondaryContext = new CSimContext(); m_SecondaryContext->m_Terrain = m_SecondaryTerrain; delete m_SecondaryComponentManager; - m_SecondaryComponentManager = new CComponentManager(*m_SecondaryContext, scriptInterface.GetRuntime()); + m_SecondaryComponentManager = new CComponentManager(*m_SecondaryContext, scriptInterface.GetContext()); m_SecondaryComponentManager->LoadComponentTypes(); delete m_SecondaryLoadedScripts; m_SecondaryLoadedScripts = new std::set(); ENSURE(LoadDefaultScripts(*m_SecondaryComponentManager, m_SecondaryLoadedScripts)); ResetComponentState(*m_SecondaryComponentManager, false, false); // Load the trigger scripts after we have loaded the simulation. { ScriptInterface::Request rq2(m_SecondaryComponentManager->GetScriptInterface()); JS::RootedValue mapSettingsCloned(rq2.cx, m_SecondaryComponentManager->GetScriptInterface().CloneValueFromOtherCompartment( scriptInterface, m_MapSettings)); ENSURE(LoadTriggerScripts(*m_SecondaryComponentManager, mapSettingsCloned, m_SecondaryLoadedScripts)); } // Load the map into the secondary simulation LDR_BeginRegistering(); std::unique_ptr mapReader(new CMapReader); std::string mapType; scriptInterface.GetProperty(m_InitAttributes, "mapType", mapType); if (mapType == "random") { // TODO: support random map scripts debug_warn(L"Serialization test mode does not support random maps"); } else { std::wstring mapFile; scriptInterface.GetProperty(m_InitAttributes, "map", mapFile); VfsPath mapfilename = VfsPath(mapFile).ChangeExtension(L".pmp"); mapReader->LoadMap(mapfilename, scriptInterface.GetJSRuntime(), JS::UndefinedHandleValue, m_SecondaryTerrain, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, m_SecondaryContext, INVALID_PLAYER, true); // throws exception on failure } LDR_EndRegistering(); ENSURE(LDR_NonprogressiveLoad() == INFO::OK); ENSURE(m_SecondaryComponentManager->DeserializeState(primaryStateBefore.state)); } if (m_EnableSerializationTest || m_TestingRejoin) { SerializationTestState secondaryStateBefore; ENSURE(m_SecondaryComponentManager->SerializeState(secondaryStateBefore.state)); if (serializationTestDebugDump) ENSURE(m_SecondaryComponentManager->DumpDebugState(secondaryStateBefore.debug, false)); if (serializationTestHash) ENSURE(m_SecondaryComponentManager->ComputeStateHash(secondaryStateBefore.hash, false)); if (primaryStateBefore.state.str() != secondaryStateBefore.state.str() || primaryStateBefore.hash != secondaryStateBefore.hash) { ReportSerializationFailure(&primaryStateBefore, NULL, &secondaryStateBefore, NULL); } SerializationTestState primaryStateAfter; ENSURE(m_ComponentManager.SerializeState(primaryStateAfter.state)); if (serializationTestHash) ENSURE(m_ComponentManager.ComputeStateHash(primaryStateAfter.hash, false)); UpdateComponents(*m_SecondaryContext, turnLengthFixed, CloneCommandsFromOtherContext(scriptInterface, m_SecondaryComponentManager->GetScriptInterface(), commands)); SerializationTestState secondaryStateAfter; ENSURE(m_SecondaryComponentManager->SerializeState(secondaryStateAfter.state)); if (serializationTestHash) ENSURE(m_SecondaryComponentManager->ComputeStateHash(secondaryStateAfter.hash, false)); if (primaryStateAfter.state.str() != secondaryStateAfter.state.str() || primaryStateAfter.hash != secondaryStateAfter.hash) { // Only do the (slow) dumping now we know we're going to need to report it ENSURE(m_ComponentManager.DumpDebugState(primaryStateAfter.debug, false)); ENSURE(m_SecondaryComponentManager->DumpDebugState(secondaryStateAfter.debug, false)); ReportSerializationFailure(&primaryStateBefore, &primaryStateAfter, &secondaryStateBefore, &secondaryStateAfter); } } // Run the GC occasionally // No delay because a lot of garbage accumulates in one turn and in non-visual replays there are // much more turns in the same time than in normal games. // Every 500 turns we run a shrinking GC, which decommits unused memory and frees all JIT code. // Based on testing, this seems to be a good compromise between memory usage and performance. // Also check the comment about gcPreserveCode in the ScriptInterface code and this forum topic: // http://www.wildfiregames.com/forum/index.php?showtopic=18466&p=300323 // // (TODO: we ought to schedule this for a frame where we're not // running the sim update, to spread the load) if (m_TurnNumber % 500 == 0) - scriptInterface.GetRuntime()->ShrinkingGC(); + scriptInterface.GetContext()->ShrinkingGC(); else - scriptInterface.GetRuntime()->MaybeIncrementalGC(0.0f); + scriptInterface.GetContext()->MaybeIncrementalGC(0.0f); if (m_EnableOOSLog) DumpState(); // Start computing AI for the next turn CmpPtr cmpAIManager(m_SimContext, SYSTEM_ENTITY); if (cmpAIManager) cmpAIManager->StartComputation(); ++m_TurnNumber; } void CSimulation2Impl::UpdateComponents(CSimContext& simContext, fixed turnLengthFixed, const std::vector& commands) { // TODO: the update process is pretty ugly, with lots of messages and dependencies // between different components. Ought to work out a nicer way to do this. CComponentManager& componentManager = simContext.GetComponentManager(); CmpPtr cmpPathfinder(simContext, SYSTEM_ENTITY); if (cmpPathfinder) cmpPathfinder->FetchAsyncResultsAndSendMessages(); { PROFILE2("Sim - Update Start"); CMessageTurnStart msgTurnStart; componentManager.BroadcastMessage(msgTurnStart); } // Push AI commands onto the queue before we use them CmpPtr cmpAIManager(simContext, SYSTEM_ENTITY); if (cmpAIManager) cmpAIManager->PushCommands(); CmpPtr cmpCommandQueue(simContext, SYSTEM_ENTITY); if (cmpCommandQueue) cmpCommandQueue->FlushTurn(commands); // Process newly generated move commands so the UI feels snappy if (cmpPathfinder) { cmpPathfinder->StartProcessingMoves(true); cmpPathfinder->FetchAsyncResultsAndSendMessages(); } // Send all the update phases { PROFILE2("Sim - Update"); CMessageUpdate msgUpdate(turnLengthFixed); componentManager.BroadcastMessage(msgUpdate); } { CMessageUpdate_MotionFormation msgUpdate(turnLengthFixed); componentManager.BroadcastMessage(msgUpdate); } // Process move commands for formations (group proxy) if (cmpPathfinder) { cmpPathfinder->StartProcessingMoves(true); cmpPathfinder->FetchAsyncResultsAndSendMessages(); } { PROFILE2("Sim - Motion Unit"); CMessageUpdate_MotionUnit msgUpdate(turnLengthFixed); componentManager.BroadcastMessage(msgUpdate); } { PROFILE2("Sim - Update Final"); CMessageUpdate_Final msgUpdate(turnLengthFixed); componentManager.BroadcastMessage(msgUpdate); } // Clean up any entities destroyed during the simulation update componentManager.FlushDestroyedComponents(); // Process all remaining moves if (cmpPathfinder) { cmpPathfinder->UpdateGrid(); cmpPathfinder->StartProcessingMoves(false); } } void CSimulation2Impl::Interpolate(float simFrameLength, float frameOffset, float realFrameLength) { PROFILE3("sim interpolate"); m_LastFrameOffset = frameOffset; CMessageInterpolate msg(simFrameLength, frameOffset, realFrameLength); m_ComponentManager.BroadcastMessage(msg); // Clean up any entities destroyed during interpolate (e.g. local corpses) m_ComponentManager.FlushDestroyedComponents(); } void CSimulation2Impl::DumpState() { PROFILE("DumpState"); std::stringstream name;\ name << std::setw(5) << std::setfill('0') << m_TurnNumber << ".txt"; const OsPath path = m_OOSLogPath / name.str(); std::ofstream file (OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc); if (!DirectoryExists(m_OOSLogPath)) { LOGWARNING("OOS-log directory %s was deleted, creating it again.", m_OOSLogPath.string8().c_str()); CreateDirectories(m_OOSLogPath, 0700); } file << "State hash: " << std::hex; std::string hashRaw; m_ComponentManager.ComputeStateHash(hashRaw, false); for (size_t i = 0; i < hashRaw.size(); ++i) file << std::setfill('0') << std::setw(2) << (int)(unsigned char)hashRaw[i]; file << std::dec << "\n"; file << "\n"; m_ComponentManager.DumpDebugState(file, true); std::ofstream binfile (OsString(path.ChangeExtension(L".dat")).c_str(), std::ofstream::out | std::ofstream::trunc | std::ofstream::binary); m_ComponentManager.SerializeState(binfile); } //////////////////////////////////////////////////////////////// -CSimulation2::CSimulation2(CUnitManager* unitManager, shared_ptr rt, CTerrain* terrain) : +CSimulation2::CSimulation2(CUnitManager* unitManager, shared_ptr rt, CTerrain* terrain) : m(new CSimulation2Impl(unitManager, rt, terrain)) { } CSimulation2::~CSimulation2() { delete m; } // Forward all method calls to the appropriate CSimulation2Impl/CComponentManager methods: void CSimulation2::EnableSerializationTest() { m->m_EnableSerializationTest = true; } void CSimulation2::EnableRejoinTest(int rejoinTestTurn) { m->m_RejoinTestTurn = rejoinTestTurn; } void CSimulation2::EnableOOSLog() { if (m->m_EnableOOSLog) return; m->m_EnableOOSLog = true; m->m_OOSLogPath = createDateIndexSubdirectory(psLogDir() / "oos_logs"); debug_printf("Writing ooslogs to %s\n", m->m_OOSLogPath.string8().c_str()); } entity_id_t CSimulation2::AddEntity(const std::wstring& templateName) { return m->m_ComponentManager.AddEntity(templateName, m->m_ComponentManager.AllocateNewEntity()); } entity_id_t CSimulation2::AddEntity(const std::wstring& templateName, entity_id_t preferredId) { return m->m_ComponentManager.AddEntity(templateName, m->m_ComponentManager.AllocateNewEntity(preferredId)); } entity_id_t CSimulation2::AddLocalEntity(const std::wstring& templateName) { return m->m_ComponentManager.AddEntity(templateName, m->m_ComponentManager.AllocateNewLocalEntity()); } void CSimulation2::DestroyEntity(entity_id_t ent) { m->m_ComponentManager.DestroyComponentsSoon(ent); } void CSimulation2::FlushDestroyedEntities() { m->m_ComponentManager.FlushDestroyedComponents(); } IComponent* CSimulation2::QueryInterface(entity_id_t ent, int iid) const { return m->m_ComponentManager.QueryInterface(ent, iid); } void CSimulation2::PostMessage(entity_id_t ent, const CMessage& msg) const { m->m_ComponentManager.PostMessage(ent, msg); } void CSimulation2::BroadcastMessage(const CMessage& msg) const { m->m_ComponentManager.BroadcastMessage(msg); } CSimulation2::InterfaceList CSimulation2::GetEntitiesWithInterface(int iid) { return m->m_ComponentManager.GetEntitiesWithInterface(iid); } const CSimulation2::InterfaceListUnordered& CSimulation2::GetEntitiesWithInterfaceUnordered(int iid) { return m->m_ComponentManager.GetEntitiesWithInterfaceUnordered(iid); } const CSimContext& CSimulation2::GetSimContext() const { return m->m_SimContext; } ScriptInterface& CSimulation2::GetScriptInterface() const { return m->m_ComponentManager.GetScriptInterface(); } void CSimulation2::PreInitGame() { ScriptInterface::Request rq(GetScriptInterface()); JS::RootedValue global(rq.cx, rq.globalValue()); GetScriptInterface().CallFunctionVoid(global, "PreInitGame"); } void CSimulation2::InitGame() { ScriptInterface::Request rq(GetScriptInterface()); JS::RootedValue global(rq.cx, rq.globalValue()); JS::RootedValue settings(rq.cx); JS::RootedValue tmpInitAttributes(rq.cx, GetInitAttributes()); GetScriptInterface().GetProperty(tmpInitAttributes, "settings", &settings); GetScriptInterface().CallFunctionVoid(global, "InitGame", settings); } void CSimulation2::Update(int turnLength) { std::vector commands; m->Update(turnLength, commands); } void CSimulation2::Update(int turnLength, const std::vector& commands) { m->Update(turnLength, commands); } void CSimulation2::Interpolate(float simFrameLength, float frameOffset, float realFrameLength) { m->Interpolate(simFrameLength, frameOffset, realFrameLength); } void CSimulation2::RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling) { PROFILE3("sim submit"); CMessageRenderSubmit msg(collector, frustum, culling); m->m_ComponentManager.BroadcastMessage(msg); } float CSimulation2::GetLastFrameOffset() const { return m->m_LastFrameOffset; } bool CSimulation2::LoadScripts(const VfsPath& path) { return m->LoadScripts(m->m_ComponentManager, &m->m_LoadedScripts, path); } bool CSimulation2::LoadDefaultScripts() { return m->LoadDefaultScripts(m->m_ComponentManager, &m->m_LoadedScripts); } void CSimulation2::SetStartupScript(const std::string& code) { m->m_StartupScript = code; } const std::string& CSimulation2::GetStartupScript() { return m->m_StartupScript; } void CSimulation2::SetInitAttributes(JS::HandleValue attribs) { m->m_InitAttributes = attribs; } JS::Value CSimulation2::GetInitAttributes() { return m->m_InitAttributes.get(); } void CSimulation2::GetInitAttributes(JS::MutableHandleValue ret) { ret.set(m->m_InitAttributes); } void CSimulation2::SetMapSettings(const std::string& settings) { m->m_ComponentManager.GetScriptInterface().ParseJSON(settings, &m->m_MapSettings); } void CSimulation2::SetMapSettings(JS::HandleValue settings) { m->m_MapSettings = settings; m->InitRNGSeedSimulation(); m->InitRNGSeedAI(); } std::string CSimulation2::GetMapSettingsString() { return m->m_ComponentManager.GetScriptInterface().StringifyJSON(&m->m_MapSettings); } void CSimulation2::GetMapSettings(JS::MutableHandleValue ret) { ret.set(m->m_MapSettings); } void CSimulation2::LoadPlayerSettings(bool newPlayers) { ScriptInterface::Request rq(GetScriptInterface()); JS::RootedValue global(rq.cx, rq.globalValue()); GetScriptInterface().CallFunctionVoid(global, "LoadPlayerSettings", m->m_MapSettings, newPlayers); } void CSimulation2::LoadMapSettings() { ScriptInterface::Request rq(GetScriptInterface()); JS::RootedValue global(rq.cx, rq.globalValue()); // Initialize here instead of in Update() GetScriptInterface().CallFunctionVoid(global, "LoadMapSettings", m->m_MapSettings); GetScriptInterface().FreezeObject(m->m_InitAttributes, true); GetScriptInterface().SetGlobal("InitAttributes", m->m_InitAttributes, true, true, true); if (!m->m_StartupScript.empty()) GetScriptInterface().LoadScript(L"map startup script", m->m_StartupScript); // Load the trigger scripts after we have loaded the simulation and the map. m->LoadTriggerScripts(m->m_ComponentManager, m->m_MapSettings, &m->m_LoadedScripts); } int CSimulation2::ProgressiveLoad() { return m->ProgressiveLoad(); } Status CSimulation2::ReloadChangedFile(const VfsPath& path) { return m->ReloadChangedFile(path); } void CSimulation2::ResetState(bool skipScriptedComponents, bool skipAI) { m->ResetState(skipScriptedComponents, skipAI); } bool CSimulation2::ComputeStateHash(std::string& outHash, bool quick) { return m->m_ComponentManager.ComputeStateHash(outHash, quick); } bool CSimulation2::DumpDebugState(std::ostream& stream) { return m->m_ComponentManager.DumpDebugState(stream, true); } bool CSimulation2::SerializeState(std::ostream& stream) { return m->m_ComponentManager.SerializeState(stream); } bool CSimulation2::DeserializeState(std::istream& stream) { // TODO: need to make sure the required SYSTEM_ENTITY components get constructed return m->m_ComponentManager.DeserializeState(stream); } std::string CSimulation2::GenerateSchema() { return m->m_ComponentManager.GenerateSchema(); } static std::vector GetJSONData(const VfsPath& path) { VfsPaths pathnames; Status ret = vfs::GetPathnames(g_VFS, path, L"*.json", pathnames); if (ret != INFO::OK) { // Some error reading directory wchar_t error[200]; LOGERROR("Error reading directory '%s': %s", path.string8(), utf8_from_wstring(StatusDescription(ret, error, ARRAY_SIZE(error)))); return std::vector(); } std::vector data; for (const VfsPath& p : pathnames) { // Load JSON file CVFSFile file; PSRETURN ret = file.Load(g_VFS, p); if (ret != PSRETURN_OK) { LOGERROR("GetJSONData: Failed to load file '%s': %s", p.string8(), GetErrorString(ret)); continue; } data.push_back(file.DecodeUTF8()); // assume it's UTF-8 } return data; } std::vector CSimulation2::GetRMSData() { return GetJSONData(L"maps/random/"); } std::vector CSimulation2::GetCivData() { return GetJSONData(L"simulation/data/civs/"); } std::vector CSimulation2::GetVictoryConditiondData() { return GetJSONData(L"simulation/data/settings/victory_conditions/"); } static std::string ReadJSON(const VfsPath& path) { if (!VfsFileExists(path)) { LOGERROR("File '%s' does not exist", path.string8()); return std::string(); } // Load JSON file CVFSFile file; PSRETURN ret = file.Load(g_VFS, path); if (ret != PSRETURN_OK) { LOGERROR("Failed to load file '%s': %s", path.string8(), GetErrorString(ret)); return std::string(); } return file.DecodeUTF8(); // assume it's UTF-8 } std::string CSimulation2::GetPlayerDefaults() { return ReadJSON(L"simulation/data/settings/player_defaults.json"); } std::string CSimulation2::GetMapSizes() { return ReadJSON(L"simulation/data/settings/map_sizes.json"); } std::string CSimulation2::GetAIData() { const ScriptInterface& scriptInterface = GetScriptInterface(); ScriptInterface::Request rq(scriptInterface); JS::RootedValue aiData(rq.cx, ICmpAIManager::GetAIs(scriptInterface)); // Build single JSON string with array of AI data JS::RootedValue ais(rq.cx); if (!ScriptInterface::CreateObject(rq, &ais, "AIData", aiData)) return std::string(); return scriptInterface.StringifyJSON(&ais); } Index: ps/trunk/source/simulation2/components/CCmpAIManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpAIManager.cpp (revision 24180) +++ ps/trunk/source/simulation2/components/CCmpAIManager.cpp (revision 24181) @@ -1,1158 +1,1158 @@ /* Copyright (C) 2020 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 "simulation2/system/Component.h" #include "ICmpAIManager.h" #include "simulation2/MessageTypes.h" #include "graphics/Terrain.h" #include "lib/timer.h" #include "lib/tex/tex.h" #include "lib/allocators/shared_ptr.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Profile.h" #include "ps/scripting/JSInterface_VFS.h" #include "ps/TemplateLoader.h" #include "ps/Util.h" -#include "scriptinterface/ScriptRuntime.h" +#include "scriptinterface/ScriptContext.h" #include "simulation2/components/ICmpAIInterface.h" #include "simulation2/components/ICmpCommandQueue.h" #include "simulation2/components/ICmpObstructionManager.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/components/ICmpTemplateManager.h" #include "simulation2/components/ICmpTerritoryManager.h" #include "simulation2/helpers/HierarchicalPathfinder.h" #include "simulation2/helpers/LongPathfinder.h" #include "simulation2/serialization/DebugSerializer.h" #include "simulation2/serialization/StdDeserializer.h" #include "simulation2/serialization/StdSerializer.h" #include "simulation2/serialization/SerializeTemplates.h" extern void QuitEngine(); /** * @file * Player AI interface. * AI is primarily scripted, and the CCmpAIManager component defined here * takes care of managing all the scripts. * * To avoid slow AI scripts causing jerky rendering, they are run in a background * thread (maintained by CAIWorker) so that it's okay if they take a whole simulation * turn before returning their results (though preferably they shouldn't use nearly * that much CPU). * * CCmpAIManager grabs the world state after each turn (making use of AIInterface.js * and AIProxy.js to decide what data to include) then passes it to CAIWorker. * The AI scripts will then run asynchronously and return a list of commands to execute. * Any attempts to read the command list (including indirectly via serialization) * will block until it's actually completed, so the rest of the engine should avoid * reading it for as long as possible. * * JS::Values are passed between the game and AI threads using ScriptInterface::StructuredClone. * * TODO: actually the thread isn't implemented yet, because performance hasn't been * sufficiently problematic to justify the complexity yet, but the CAIWorker interface * is designed to hopefully support threading when we want it. */ /** * Implements worker thread for CCmpAIManager. */ class CAIWorker { private: class CAIPlayer { NONCOPYABLE(CAIPlayer); public: CAIPlayer(CAIWorker& worker, const std::wstring& aiName, player_id_t player, u8 difficulty, const std::wstring& behavior, shared_ptr scriptInterface) : m_Worker(worker), m_AIName(aiName), m_Player(player), m_Difficulty(difficulty), m_Behavior(behavior), m_ScriptInterface(scriptInterface), m_Obj(scriptInterface->GetJSRuntime()) { } bool Initialise() { // LoadScripts will only load each script once even though we call it for each player if (!m_Worker.LoadScripts(m_AIName)) return false; ScriptInterface::Request rq(m_ScriptInterface); OsPath path = L"simulation/ai/" + m_AIName + L"/data.json"; JS::RootedValue metadata(rq.cx); m_Worker.LoadMetadata(path, &metadata); if (metadata.isUndefined()) { LOGERROR("Failed to create AI player: can't find %s", path.string8()); return false; } // Get the constructor name from the metadata std::string moduleName; std::string constructor; JS::RootedValue objectWithConstructor(rq.cx); // object that should contain the constructor function JS::RootedValue global(rq.cx, rq.globalValue()); JS::RootedValue ctor(rq.cx); if (!m_ScriptInterface->HasProperty(metadata, "moduleName")) { LOGERROR("Failed to create AI player: %s: missing 'moduleName'", path.string8()); return false; } m_ScriptInterface->GetProperty(metadata, "moduleName", moduleName); if (!m_ScriptInterface->GetProperty(global, moduleName.c_str(), &objectWithConstructor) || objectWithConstructor.isUndefined()) { LOGERROR("Failed to create AI player: %s: can't find the module that should contain the constructor: '%s'", path.string8(), moduleName); return false; } if (!m_ScriptInterface->GetProperty(metadata, "constructor", constructor)) { LOGERROR("Failed to create AI player: %s: missing 'constructor'", path.string8()); return false; } // Get the constructor function from the loaded scripts if (!m_ScriptInterface->GetProperty(objectWithConstructor, constructor.c_str(), &ctor) || ctor.isNull()) { LOGERROR("Failed to create AI player: %s: can't find constructor '%s'", path.string8(), constructor); return false; } m_ScriptInterface->GetProperty(metadata, "useShared", m_UseSharedComponent); // Set up the data to pass as the constructor argument JS::RootedValue settings(rq.cx); ScriptInterface::CreateObject( rq, &settings, "player", m_Player, "difficulty", m_Difficulty, "behavior", m_Behavior); if (!m_UseSharedComponent) { ENSURE(m_Worker.m_HasLoadedEntityTemplates); m_ScriptInterface->SetProperty(settings, "templates", m_Worker.m_EntityTemplates, false); } JS::AutoValueVector argv(rq.cx); argv.append(settings.get()); m_ScriptInterface->CallConstructor(ctor, argv, &m_Obj); if (m_Obj.get().isNull()) { LOGERROR("Failed to create AI player: %s: error calling constructor '%s'", path.string8(), constructor); return false; } return true; } void Run(JS::HandleValue state, int playerID) { m_Commands.clear(); m_ScriptInterface->CallFunctionVoid(m_Obj, "HandleMessage", state, playerID); } // overloaded with a sharedAI part. // javascript can handle both natively on the same function. void Run(JS::HandleValue state, int playerID, JS::HandleValue SharedAI) { m_Commands.clear(); m_ScriptInterface->CallFunctionVoid(m_Obj, "HandleMessage", state, playerID, SharedAI); } void InitAI(JS::HandleValue state, JS::HandleValue SharedAI) { m_Commands.clear(); m_ScriptInterface->CallFunctionVoid(m_Obj, "Init", state, m_Player, SharedAI); } CAIWorker& m_Worker; std::wstring m_AIName; player_id_t m_Player; u8 m_Difficulty; std::wstring m_Behavior; bool m_UseSharedComponent; // Take care to keep this declaration before heap rooted members. Destructors of heap rooted - // members have to be called before the runtime destructor. + // members have to be called before the context destructor. shared_ptr m_ScriptInterface; JS::PersistentRootedValue m_Obj; std::vector > m_Commands; }; public: struct SCommandSets { player_id_t player; std::vector > commands; }; CAIWorker() : - m_ScriptInterface(new ScriptInterface("Engine", "AI", g_ScriptRuntime)), + m_ScriptInterface(new ScriptInterface("Engine", "AI", g_ScriptContext)), m_TurnNum(0), m_CommandsComputed(true), m_HasLoadedEntityTemplates(false), m_HasSharedComponent(false), - m_EntityTemplates(g_ScriptRuntime->GetJSRuntime()), - m_SharedAIObj(g_ScriptRuntime->GetJSRuntime()), - m_PassabilityMapVal(g_ScriptRuntime->GetJSRuntime()), - m_TerritoryMapVal(g_ScriptRuntime->GetJSRuntime()) + m_EntityTemplates(g_ScriptContext->GetJSRuntime()), + m_SharedAIObj(g_ScriptContext->GetJSRuntime()), + m_PassabilityMapVal(g_ScriptContext->GetJSRuntime()), + m_TerritoryMapVal(g_ScriptContext->GetJSRuntime()) { m_ScriptInterface->ReplaceNondeterministicRNG(m_RNG); m_ScriptInterface->SetCallbackData(static_cast (this)); JS_AddExtraGCRootsTracer(m_ScriptInterface->GetJSRuntime(), Trace, this); m_ScriptInterface->RegisterFunction("PostCommand"); m_ScriptInterface->RegisterFunction("IncludeModule"); m_ScriptInterface->RegisterFunction("Exit"); m_ScriptInterface->RegisterFunction("ComputePath"); m_ScriptInterface->RegisterFunction, u32, u32, u32, CAIWorker::DumpImage>("DumpImage"); m_ScriptInterface->RegisterFunction("GetTemplate"); JSI_VFS::RegisterScriptFunctions_Simulation(*(m_ScriptInterface.get())); // Globalscripts may use VFS script functions m_ScriptInterface->LoadGlobalScripts(); } ~CAIWorker() { JS_RemoveExtraGCRootsTracer(m_ScriptInterface->GetJSRuntime(), Trace, this); } bool HasLoadedEntityTemplates() const { return m_HasLoadedEntityTemplates; } bool LoadScripts(const std::wstring& moduleName) { // Ignore modules that are already loaded if (m_LoadedModules.find(moduleName) != m_LoadedModules.end()) return true; // Mark this as loaded, to prevent it recursively loading itself m_LoadedModules.insert(moduleName); // Load and execute *.js VfsPaths pathnames; if (vfs::GetPathnames(g_VFS, L"simulation/ai/" + moduleName + L"/", L"*.js", pathnames) < 0) { LOGERROR("Failed to load AI scripts for module %s", utf8_from_wstring(moduleName)); return false; } for (const VfsPath& path : pathnames) { if (!m_ScriptInterface->LoadGlobalScriptFile(path)) { LOGERROR("Failed to load script %s", path.string8()); return false; } } return true; } static void IncludeModule(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& name) { ENSURE(pCmptPrivate->pCBData); CAIWorker* self = static_cast (pCmptPrivate->pCBData); self->LoadScripts(name); } static void PostCommand(ScriptInterface::CmptPrivate* pCmptPrivate, int playerid, JS::HandleValue cmd) { ENSURE(pCmptPrivate->pCBData); CAIWorker* self = static_cast (pCmptPrivate->pCBData); self->PostCommand(playerid, cmd); } void PostCommand(int playerid, JS::HandleValue cmd) { for (size_t i=0; im_Player == playerid) { m_Players[i]->m_Commands.push_back(m_ScriptInterface->WriteStructuredClone(cmd)); return; } } LOGERROR("Invalid playerid in PostCommand!"); } static JS::Value ComputePath(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue position, JS::HandleValue goal, pass_class_t passClass) { ENSURE(pCmptPrivate->pCBData); CAIWorker* self = static_cast (pCmptPrivate->pCBData); ScriptInterface::Request rq(self->m_ScriptInterface); CFixedVector2D pos, goalPos; std::vector waypoints; JS::RootedValue retVal(rq.cx); self->m_ScriptInterface->FromJSVal(rq, position, pos); self->m_ScriptInterface->FromJSVal(rq, goal, goalPos); self->ComputePath(pos, goalPos, passClass, waypoints); self->m_ScriptInterface->ToJSVal >(rq, &retVal, waypoints); return retVal; } void ComputePath(const CFixedVector2D& pos, const CFixedVector2D& goal, pass_class_t passClass, std::vector& waypoints) { WaypointPath ret; PathGoal pathGoal = { PathGoal::POINT, goal.X, goal.Y }; m_LongPathfinder.ComputePath(m_HierarchicalPathfinder, pos.X, pos.Y, pathGoal, passClass, ret); for (Waypoint& wp : ret.m_Waypoints) waypoints.emplace_back(wp.x, wp.z); } static CParamNode GetTemplate(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& name) { ENSURE(pCmptPrivate->pCBData); CAIWorker* self = static_cast (pCmptPrivate->pCBData); return self->GetTemplate(name); } CParamNode GetTemplate(const std::string& name) { if (!m_TemplateLoader.TemplateExists(name)) return CParamNode(false); return m_TemplateLoader.GetTemplateFileData(name).GetChild("Entity"); } static void ExitProgram(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) { QuitEngine(); } /** * Debug function for AI scripts to dump 2D array data (e.g. terrain tile weights). */ static void DumpImage(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& name, const std::vector& data, u32 w, u32 h, u32 max) { // TODO: this is totally not threadsafe. VfsPath filename = L"screenshots/aidump/" + name; if (data.size() != w*h) { debug_warn(L"DumpImage: data size doesn't match w*h"); return; } if (max == 0) { debug_warn(L"DumpImage: max must not be 0"); return; } const size_t bpp = 8; int flags = TEX_BOTTOM_UP|TEX_GREY; const size_t img_size = w * h * bpp/8; const size_t hdr_size = tex_hdr_size(filename); shared_ptr buf; AllocateAligned(buf, hdr_size+img_size, maxSectorSize); Tex t; if (t.wrap(w, h, bpp, flags, buf, hdr_size) < 0) return; u8* img = buf.get() + hdr_size; for (size_t i = 0; i < data.size(); ++i) img[i] = (u8)((data[i] * 255) / max); tex_write(&t, filename); } void SetRNGSeed(u32 seed) { m_RNG.seed(seed); } bool TryLoadSharedComponent() { ScriptInterface::Request rq(m_ScriptInterface); // we don't need to load it. if (!m_HasSharedComponent) return false; // reset the value so it can be used to determine if we actually initialized it. m_HasSharedComponent = false; if (LoadScripts(L"common-api")) m_HasSharedComponent = true; else return false; // mainly here for the error messages OsPath path = L"simulation/ai/common-api/"; // Constructor name is SharedScript, it's in the module API3 // TODO: Hardcoding this is bad, we need a smarter way. JS::RootedValue AIModule(rq.cx); JS::RootedValue global(rq.cx, rq.globalValue()); JS::RootedValue ctor(rq.cx); if (!m_ScriptInterface->GetProperty(global, "API3", &AIModule) || AIModule.isUndefined()) { LOGERROR("Failed to create shared AI component: %s: can't find module '%s'", path.string8(), "API3"); return false; } if (!m_ScriptInterface->GetProperty(AIModule, "SharedScript", &ctor) || ctor.isUndefined()) { LOGERROR("Failed to create shared AI component: %s: can't find constructor '%s'", path.string8(), "SharedScript"); return false; } // Set up the data to pass as the constructor argument JS::RootedValue playersID(rq.cx); ScriptInterface::CreateObject(rq, &playersID); for (size_t i = 0; i < m_Players.size(); ++i) { JS::RootedValue val(rq.cx); m_ScriptInterface->ToJSVal(rq, &val, m_Players[i]->m_Player); m_ScriptInterface->SetPropertyInt(playersID, i, val, true); } ENSURE(m_HasLoadedEntityTemplates); JS::RootedValue settings(rq.cx); ScriptInterface::CreateObject( rq, &settings, "players", playersID, "templates", m_EntityTemplates); JS::AutoValueVector argv(rq.cx); argv.append(settings); m_ScriptInterface->CallConstructor(ctor, argv, &m_SharedAIObj); if (m_SharedAIObj.get().isNull()) { LOGERROR("Failed to create shared AI component: %s: error calling constructor '%s'", path.string8(), "SharedScript"); return false; } return true; } bool AddPlayer(const std::wstring& aiName, player_id_t player, u8 difficulty, const std::wstring& behavior) { shared_ptr ai(new CAIPlayer(*this, aiName, player, difficulty, behavior, m_ScriptInterface)); if (!ai->Initialise()) return false; // this will be set to true if we need to load the shared Component. if (!m_HasSharedComponent) m_HasSharedComponent = ai->m_UseSharedComponent; m_Players.push_back(ai); return true; } bool RunGamestateInit(const shared_ptr& gameState, const Grid& passabilityMap, const Grid& territoryMap, const std::map& nonPathfindingPassClassMasks, const std::map& pathfindingPassClassMasks) { // this will be run last by InitGame.js, passing the full game representation. // For now it will run for the shared Component. // This is NOT run during deserialization. ScriptInterface::Request rq(m_ScriptInterface); JS::RootedValue state(rq.cx); m_ScriptInterface->ReadStructuredClone(gameState, &state); ScriptInterface::ToJSVal(rq, &m_PassabilityMapVal, passabilityMap); ScriptInterface::ToJSVal(rq, &m_TerritoryMapVal, territoryMap); m_PassabilityMap = passabilityMap; m_NonPathfindingPassClasses = nonPathfindingPassClassMasks; m_PathfindingPassClasses = pathfindingPassClassMasks; m_LongPathfinder.Reload(&m_PassabilityMap); m_HierarchicalPathfinder.Recompute(&m_PassabilityMap, nonPathfindingPassClassMasks, pathfindingPassClassMasks); if (m_HasSharedComponent) { m_ScriptInterface->SetProperty(state, "passabilityMap", m_PassabilityMapVal, true); m_ScriptInterface->SetProperty(state, "territoryMap", m_TerritoryMapVal, true); m_ScriptInterface->CallFunctionVoid(m_SharedAIObj, "init", state); for (size_t i = 0; i < m_Players.size(); ++i) { if (m_HasSharedComponent && m_Players[i]->m_UseSharedComponent) m_Players[i]->InitAI(state, m_SharedAIObj); } } return true; } void UpdateGameState(const shared_ptr& gameState) { ENSURE(m_CommandsComputed); m_GameState = gameState; } void UpdatePathfinder(const Grid& passabilityMap, bool globallyDirty, const Grid& dirtinessGrid, bool justDeserialized, const std::map& nonPathfindingPassClassMasks, const std::map& pathfindingPassClassMasks) { ENSURE(m_CommandsComputed); bool dimensionChange = m_PassabilityMap.m_W != passabilityMap.m_W || m_PassabilityMap.m_H != passabilityMap.m_H; m_PassabilityMap = passabilityMap; if (globallyDirty) { m_LongPathfinder.Reload(&m_PassabilityMap); m_HierarchicalPathfinder.Recompute(&m_PassabilityMap, nonPathfindingPassClassMasks, pathfindingPassClassMasks); } else { m_LongPathfinder.Update(&m_PassabilityMap); m_HierarchicalPathfinder.Update(&m_PassabilityMap, dirtinessGrid); } ScriptInterface::Request rq(m_ScriptInterface); if (dimensionChange || justDeserialized) ScriptInterface::ToJSVal(rq, &m_PassabilityMapVal, m_PassabilityMap); else { // Avoid a useless memory reallocation followed by a garbage collection. JS::RootedObject mapObj(rq.cx, &m_PassabilityMapVal.toObject()); JS::RootedValue mapData(rq.cx); ENSURE(JS_GetProperty(rq.cx, mapObj, "data", &mapData)); JS::RootedObject dataObj(rq.cx, &mapData.toObject()); u32 length = 0; ENSURE(JS_GetArrayLength(rq.cx, dataObj, &length)); u32 nbytes = (u32)(length * sizeof(NavcellData)); bool sharedMemory; JS::AutoCheckCannotGC nogc; memcpy((void*)JS_GetUint16ArrayData(dataObj, &sharedMemory, nogc), m_PassabilityMap.m_Data, nbytes); } } void UpdateTerritoryMap(const Grid& territoryMap) { ENSURE(m_CommandsComputed); bool dimensionChange = m_TerritoryMap.m_W != territoryMap.m_W || m_TerritoryMap.m_H != territoryMap.m_H; m_TerritoryMap = territoryMap; ScriptInterface::Request rq(m_ScriptInterface); if (dimensionChange) ScriptInterface::ToJSVal(rq, &m_TerritoryMapVal, m_TerritoryMap); else { // Avoid a useless memory reallocation followed by a garbage collection. JS::RootedObject mapObj(rq.cx, &m_TerritoryMapVal.toObject()); JS::RootedValue mapData(rq.cx); ENSURE(JS_GetProperty(rq.cx, mapObj, "data", &mapData)); JS::RootedObject dataObj(rq.cx, &mapData.toObject()); u32 length = 0; ENSURE(JS_GetArrayLength(rq.cx, dataObj, &length)); u32 nbytes = (u32)(length * sizeof(u8)); bool sharedMemory; JS::AutoCheckCannotGC nogc; memcpy((void*)JS_GetUint8ArrayData(dataObj, &sharedMemory, nogc), m_TerritoryMap.m_Data, nbytes); } } void StartComputation() { m_CommandsComputed = false; } void WaitToFinishComputation() { if (!m_CommandsComputed) { PerformComputation(); m_CommandsComputed = true; } } void GetCommands(std::vector& commands) { WaitToFinishComputation(); commands.clear(); commands.resize(m_Players.size()); for (size_t i = 0; i < m_Players.size(); ++i) { commands[i].player = m_Players[i]->m_Player; commands[i].commands = m_Players[i]->m_Commands; } } void LoadEntityTemplates(const std::vector >& templates) { ScriptInterface::Request rq(m_ScriptInterface); m_HasLoadedEntityTemplates = true; ScriptInterface::CreateObject(rq, &m_EntityTemplates); JS::RootedValue val(rq.cx); for (size_t i = 0; i < templates.size(); ++i) { templates[i].second->ToJSVal(rq, false, &val); m_ScriptInterface->SetProperty(m_EntityTemplates, templates[i].first.c_str(), val, true); } } void Serialize(std::ostream& stream, bool isDebug) { WaitToFinishComputation(); if (isDebug) { CDebugSerializer serializer(*m_ScriptInterface, stream); serializer.Indent(4); SerializeState(serializer); } else { CStdSerializer serializer(*m_ScriptInterface, stream); SerializeState(serializer); } } void SerializeState(ISerializer& serializer) { if (m_Players.empty()) return; ScriptInterface::Request rq(m_ScriptInterface); std::stringstream rngStream; rngStream << m_RNG; serializer.StringASCII("rng", rngStream.str(), 0, 32); serializer.NumberU32_Unbounded("turn", m_TurnNum); serializer.Bool("useSharedScript", m_HasSharedComponent); if (m_HasSharedComponent) { JS::RootedValue sharedData(rq.cx); if (!m_ScriptInterface->CallFunction(m_SharedAIObj, "Serialize", &sharedData)) LOGERROR("AI shared script Serialize call failed"); serializer.ScriptVal("sharedData", &sharedData); } for (size_t i = 0; i < m_Players.size(); ++i) { serializer.String("name", m_Players[i]->m_AIName, 1, 256); serializer.NumberI32_Unbounded("player", m_Players[i]->m_Player); serializer.NumberU8_Unbounded("difficulty", m_Players[i]->m_Difficulty); serializer.String("behavior", m_Players[i]->m_Behavior, 1, 256); serializer.NumberU32_Unbounded("num commands", (u32)m_Players[i]->m_Commands.size()); for (size_t j = 0; j < m_Players[i]->m_Commands.size(); ++j) { JS::RootedValue val(rq.cx); m_ScriptInterface->ReadStructuredClone(m_Players[i]->m_Commands[j], &val); serializer.ScriptVal("command", &val); } bool hasCustomSerialize = m_ScriptInterface->HasProperty(m_Players[i]->m_Obj, "Serialize"); if (hasCustomSerialize) { JS::RootedValue scriptData(rq.cx); if (!m_ScriptInterface->CallFunction(m_Players[i]->m_Obj, "Serialize", &scriptData)) LOGERROR("AI script Serialize call failed"); serializer.ScriptVal("data", &scriptData); } else { serializer.ScriptVal("data", &m_Players[i]->m_Obj); } } // AI pathfinder SerializeMap()(serializer, "non pathfinding pass classes", m_NonPathfindingPassClasses); SerializeMap()(serializer, "pathfinding pass classes", m_PathfindingPassClasses); serializer.NumberU16_Unbounded("pathfinder grid w", m_PassabilityMap.m_W); serializer.NumberU16_Unbounded("pathfinder grid h", m_PassabilityMap.m_H); serializer.RawBytes("pathfinder grid data", (const u8*)m_PassabilityMap.m_Data, m_PassabilityMap.m_W*m_PassabilityMap.m_H*sizeof(NavcellData)); } void Deserialize(std::istream& stream, u32 numAis) { m_PlayerMetadata.clear(); m_Players.clear(); if (numAis == 0) return; ScriptInterface::Request rq(m_ScriptInterface); ENSURE(m_CommandsComputed); // deserializing while we're still actively computing would be bad CStdDeserializer deserializer(*m_ScriptInterface, stream); std::string rngString; std::stringstream rngStream; deserializer.StringASCII("rng", rngString, 0, 32); rngStream << rngString; rngStream >> m_RNG; deserializer.NumberU32_Unbounded("turn", m_TurnNum); deserializer.Bool("useSharedScript", m_HasSharedComponent); if (m_HasSharedComponent) { TryLoadSharedComponent(); JS::RootedValue sharedData(rq.cx); deserializer.ScriptVal("sharedData", &sharedData); if (!m_ScriptInterface->CallFunctionVoid(m_SharedAIObj, "Deserialize", sharedData)) LOGERROR("AI shared script Deserialize call failed"); } for (size_t i = 0; i < numAis; ++i) { std::wstring name; player_id_t player; u8 difficulty; std::wstring behavior; deserializer.String("name", name, 1, 256); deserializer.NumberI32_Unbounded("player", player); deserializer.NumberU8_Unbounded("difficulty",difficulty); deserializer.String("behavior", behavior, 1, 256); if (!AddPlayer(name, player, difficulty, behavior)) throw PSERROR_Deserialize_ScriptError(); u32 numCommands; deserializer.NumberU32_Unbounded("num commands", numCommands); m_Players.back()->m_Commands.reserve(numCommands); for (size_t j = 0; j < numCommands; ++j) { JS::RootedValue val(rq.cx); deserializer.ScriptVal("command", &val); m_Players.back()->m_Commands.push_back(m_ScriptInterface->WriteStructuredClone(val)); } bool hasCustomDeserialize = m_ScriptInterface->HasProperty(m_Players.back()->m_Obj, "Deserialize"); if (hasCustomDeserialize) { JS::RootedValue scriptData(rq.cx); deserializer.ScriptVal("data", &scriptData); if (m_Players[i]->m_UseSharedComponent) { if (!m_ScriptInterface->CallFunctionVoid(m_Players.back()->m_Obj, "Deserialize", scriptData, m_SharedAIObj)) LOGERROR("AI script Deserialize call failed"); } else if (!m_ScriptInterface->CallFunctionVoid(m_Players.back()->m_Obj, "Deserialize", scriptData)) { LOGERROR("AI script deserialize() call failed"); } } else { deserializer.ScriptVal("data", &m_Players.back()->m_Obj); } } // AI pathfinder SerializeMap()(deserializer, "non pathfinding pass classes", m_NonPathfindingPassClasses); SerializeMap()(deserializer, "pathfinding pass classes", m_PathfindingPassClasses); u16 mapW, mapH; deserializer.NumberU16_Unbounded("pathfinder grid w", mapW); deserializer.NumberU16_Unbounded("pathfinder grid h", mapH); m_PassabilityMap = Grid(mapW, mapH); deserializer.RawBytes("pathfinder grid data", (u8*)m_PassabilityMap.m_Data, mapW*mapH*sizeof(NavcellData)); m_LongPathfinder.Reload(&m_PassabilityMap); m_HierarchicalPathfinder.Recompute(&m_PassabilityMap, m_NonPathfindingPassClasses, m_PathfindingPassClasses); } int getPlayerSize() { return m_Players.size(); } private: static void Trace(JSTracer *trc, void *data) { reinterpret_cast(data)->TraceMember(trc); } void TraceMember(JSTracer *trc) { for (std::pair>& metadata : m_PlayerMetadata) JS::TraceEdge(trc, &metadata.second, "CAIWorker::m_PlayerMetadata"); } void LoadMetadata(const VfsPath& path, JS::MutableHandleValue out) { if (m_PlayerMetadata.find(path) == m_PlayerMetadata.end()) { // Load and cache the AI player metadata m_ScriptInterface->ReadJSONFile(path, out); m_PlayerMetadata[path] = JS::Heap(out); return; } out.set(m_PlayerMetadata[path].get()); } void PerformComputation() { // Deserialize the game state, to pass to the AI's HandleMessage ScriptInterface::Request rq(m_ScriptInterface); JS::RootedValue state(rq.cx); { PROFILE3("AI compute read state"); m_ScriptInterface->ReadStructuredClone(m_GameState, &state); m_ScriptInterface->SetProperty(state, "passabilityMap", m_PassabilityMapVal, true); m_ScriptInterface->SetProperty(state, "territoryMap", m_TerritoryMapVal, true); } // It would be nice to do // m_ScriptInterface->FreezeObject(state.get(), true); // to prevent AI scripts accidentally modifying the state and // affecting other AI scripts they share it with. But the performance // cost is far too high, so we won't do that. // If there is a shared component, run it if (m_HasSharedComponent) { PROFILE3("AI run shared component"); m_ScriptInterface->CallFunctionVoid(m_SharedAIObj, "onUpdate", state); } for (size_t i = 0; i < m_Players.size(); ++i) { PROFILE3("AI script"); PROFILE2_ATTR("player: %d", m_Players[i]->m_Player); PROFILE2_ATTR("script: %ls", m_Players[i]->m_AIName.c_str()); if (m_HasSharedComponent && m_Players[i]->m_UseSharedComponent) m_Players[i]->Run(state, m_Players[i]->m_Player, m_SharedAIObj); else m_Players[i]->Run(state, m_Players[i]->m_Player); } } // Take care to keep this declaration before heap rooted members. Destructors of heap rooted - // members have to be called before the runtime destructor. - shared_ptr m_ScriptRuntime; + // members have to be called before the context destructor. + shared_ptr m_ScriptContext; shared_ptr m_ScriptInterface; boost::rand48 m_RNG; u32 m_TurnNum; JS::PersistentRootedValue m_EntityTemplates; bool m_HasLoadedEntityTemplates; std::map > m_PlayerMetadata; std::vector > m_Players; // use shared_ptr just to avoid copying bool m_HasSharedComponent; JS::PersistentRootedValue m_SharedAIObj; std::vector m_Commands; std::set m_LoadedModules; shared_ptr m_GameState; Grid m_PassabilityMap; JS::PersistentRootedValue m_PassabilityMapVal; Grid m_TerritoryMap; JS::PersistentRootedValue m_TerritoryMapVal; std::map m_NonPathfindingPassClasses; std::map m_PathfindingPassClasses; HierarchicalPathfinder m_HierarchicalPathfinder; LongPathfinder m_LongPathfinder; bool m_CommandsComputed; CTemplateLoader m_TemplateLoader; }; /** * Implementation of ICmpAIManager. */ class CCmpAIManager : public ICmpAIManager { public: static void ClassInit(CComponentManager& UNUSED(componentManager)) { } DEFAULT_COMPONENT_ALLOCATOR(AIManager) static std::string GetSchema() { return ""; } virtual void Init(const CParamNode& UNUSED(paramNode)) { m_TerritoriesDirtyID = 0; m_TerritoriesDirtyBlinkingID = 0; m_JustDeserialized = false; } virtual void Deinit() { } virtual void Serialize(ISerializer& serialize) { serialize.NumberU32_Unbounded("num ais", m_Worker.getPlayerSize()); // Because the AI worker uses its own ScriptInterface, we can't use the // ISerializer (which was initialised with the simulation ScriptInterface) // directly. So we'll just grab the ISerializer's stream and write to it // with an independent serializer. m_Worker.Serialize(serialize.GetStream(), serialize.IsDebug()); } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) { Init(paramNode); u32 numAis; deserialize.NumberU32_Unbounded("num ais", numAis); if (numAis > 0) LoadUsedEntityTemplates(); m_Worker.Deserialize(deserialize.GetStream(), numAis); m_JustDeserialized = true; } virtual void AddPlayer(const std::wstring& id, player_id_t player, u8 difficulty, const std::wstring& behavior) { LoadUsedEntityTemplates(); m_Worker.AddPlayer(id, player, difficulty, behavior); // AI players can cheat and see through FoW/SoD, since that greatly simplifies // their implementation. // (TODO: maybe cleverer AIs should be able to optionally retain FoW/SoD) CmpPtr cmpRangeManager(GetSystemEntity()); if (cmpRangeManager) cmpRangeManager->SetLosRevealAll(player, true); } virtual void SetRNGSeed(u32 seed) { m_Worker.SetRNGSeed(seed); } virtual void TryLoadSharedComponent() { m_Worker.TryLoadSharedComponent(); } virtual void RunGamestateInit() { const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); ScriptInterface::Request rq(scriptInterface); CmpPtr cmpAIInterface(GetSystemEntity()); ENSURE(cmpAIInterface); // Get the game state from AIInterface // We flush events from the initialization so we get a clean state now. JS::RootedValue state(rq.cx); cmpAIInterface->GetFullRepresentation(&state, true); // Get the passability data Grid dummyGrid; const Grid* passabilityMap = &dummyGrid; CmpPtr cmpPathfinder(GetSystemEntity()); if (cmpPathfinder) passabilityMap = &cmpPathfinder->GetPassabilityGrid(); // Get the territory data // Since getting the territory grid can trigger a recalculation, we check NeedUpdateAI first Grid dummyGrid2; const Grid* territoryMap = &dummyGrid2; CmpPtr cmpTerritoryManager(GetSystemEntity()); if (cmpTerritoryManager && cmpTerritoryManager->NeedUpdateAI(&m_TerritoriesDirtyID, &m_TerritoriesDirtyBlinkingID)) territoryMap = &cmpTerritoryManager->GetTerritoryGrid(); LoadPathfinderClasses(state); std::map nonPathfindingPassClassMasks, pathfindingPassClassMasks; if (cmpPathfinder) cmpPathfinder->GetPassabilityClasses(nonPathfindingPassClassMasks, pathfindingPassClassMasks); m_Worker.RunGamestateInit(scriptInterface.WriteStructuredClone(state), *passabilityMap, *territoryMap, nonPathfindingPassClassMasks, pathfindingPassClassMasks); } virtual void StartComputation() { PROFILE("AI setup"); const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); ScriptInterface::Request rq(scriptInterface); if (m_Worker.getPlayerSize() == 0) return; CmpPtr cmpAIInterface(GetSystemEntity()); ENSURE(cmpAIInterface); // Get the game state from AIInterface JS::RootedValue state(rq.cx); if (m_JustDeserialized) cmpAIInterface->GetFullRepresentation(&state, false); else cmpAIInterface->GetRepresentation(&state); LoadPathfinderClasses(state); // add the pathfinding classes to it // Update the game state m_Worker.UpdateGameState(scriptInterface.WriteStructuredClone(state)); // Update the pathfinding data CmpPtr cmpPathfinder(GetSystemEntity()); if (cmpPathfinder) { const GridUpdateInformation& dirtinessInformations = cmpPathfinder->GetAIPathfinderDirtinessInformation(); if (dirtinessInformations.dirty || m_JustDeserialized) { const Grid& passabilityMap = cmpPathfinder->GetPassabilityGrid(); std::map nonPathfindingPassClassMasks, pathfindingPassClassMasks; cmpPathfinder->GetPassabilityClasses(nonPathfindingPassClassMasks, pathfindingPassClassMasks); m_Worker.UpdatePathfinder(passabilityMap, dirtinessInformations.globallyDirty, dirtinessInformations.dirtinessGrid, m_JustDeserialized, nonPathfindingPassClassMasks, pathfindingPassClassMasks); } cmpPathfinder->FlushAIPathfinderDirtinessInformation(); } // Update the territory data // Since getting the territory grid can trigger a recalculation, we check NeedUpdateAI first CmpPtr cmpTerritoryManager(GetSystemEntity()); if (cmpTerritoryManager && (cmpTerritoryManager->NeedUpdateAI(&m_TerritoriesDirtyID, &m_TerritoriesDirtyBlinkingID) || m_JustDeserialized)) { const Grid& territoryMap = cmpTerritoryManager->GetTerritoryGrid(); m_Worker.UpdateTerritoryMap(territoryMap); } m_Worker.StartComputation(); m_JustDeserialized = false; } virtual void PushCommands() { std::vector commands; m_Worker.GetCommands(commands); CmpPtr cmpCommandQueue(GetSystemEntity()); if (!cmpCommandQueue) return; const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); ScriptInterface::Request rq(scriptInterface); JS::RootedValue clonedCommandVal(rq.cx); for (size_t i = 0; i < commands.size(); ++i) { for (size_t j = 0; j < commands[i].commands.size(); ++j) { scriptInterface.ReadStructuredClone(commands[i].commands[j], &clonedCommandVal); cmpCommandQueue->PushLocalCommand(commands[i].player, clonedCommandVal); } } } private: size_t m_TerritoriesDirtyID; size_t m_TerritoriesDirtyBlinkingID; bool m_JustDeserialized; /** * Load the templates of all entities on the map (called when adding a new AI player for a new game * or when deserializing) */ void LoadUsedEntityTemplates() { if (m_Worker.HasLoadedEntityTemplates()) return; CmpPtr cmpTemplateManager(GetSystemEntity()); ENSURE(cmpTemplateManager); std::vector templateNames = cmpTemplateManager->FindUsedTemplates(); std::vector > usedTemplates; usedTemplates.reserve(templateNames.size()); for (const std::string& name : templateNames) { const CParamNode* node = cmpTemplateManager->GetTemplateWithoutValidation(name); if (node) usedTemplates.emplace_back(name, node); } // Send the data to the worker m_Worker.LoadEntityTemplates(usedTemplates); } void LoadPathfinderClasses(JS::HandleValue state) { CmpPtr cmpPathfinder(GetSystemEntity()); if (!cmpPathfinder) return; const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); ScriptInterface::Request rq(scriptInterface); JS::RootedValue classesVal(rq.cx); ScriptInterface::CreateObject(rq, &classesVal); std::map classes; cmpPathfinder->GetPassabilityClasses(classes); for (std::map::iterator it = classes.begin(); it != classes.end(); ++it) scriptInterface.SetProperty(classesVal, it->first.c_str(), it->second, true); scriptInterface.SetProperty(state, "passabilityClasses", classesVal, true); } CAIWorker m_Worker; }; REGISTER_COMPONENT_TYPE(AIManager) Index: ps/trunk/source/simulation2/components/tests/test_CommandQueue.h =================================================================== --- ps/trunk/source/simulation2/components/tests/test_CommandQueue.h (revision 24180) +++ ps/trunk/source/simulation2/components/tests/test_CommandQueue.h (revision 24181) @@ -1,74 +1,74 @@ /* Copyright (C) 2020 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/ICmpCommandQueue.h" class TestCmpCommandQueue : public CxxTest::TestSuite { public: void setUp() { CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); } void test_basic() { - ComponentTestHelper test(g_ScriptRuntime); + ComponentTestHelper test(g_ScriptContext); ScriptInterface::Request rq(test.GetScriptInterface()); std::vector empty; ICmpCommandQueue* cmp = test.Add(CID_CommandQueue, "", SYSTEM_ENTITY); TS_ASSERT(test.GetScriptInterface().Eval("var cmds = []; function ProcessCommand(player, cmd) { cmds.push([player, cmd]); }")); JS::RootedValue cmd(rq.cx); TS_ASSERT(test.GetScriptInterface().Eval("([1,2,3])", &cmd)); cmp->PushLocalCommand(1, cmd); TS_ASSERT(test.GetScriptInterface().Eval("({\"x\":4})", &cmd)); cmp->PushLocalCommand(-1, cmd); test.Roundtrip(); // Process the first two commands cmp->FlushTurn(empty); TS_ASSERT(test.GetScriptInterface().Eval("({\"y\":5})", &cmd)); cmp->PushLocalCommand(10, cmd); // Process the next command cmp->FlushTurn(empty); // Process no commands cmp->FlushTurn(empty); test.Roundtrip(); std::string output; TS_ASSERT(test.GetScriptInterface().Eval("JSON.stringify(cmds)", output)); TS_ASSERT_STR_EQUALS(output, "[[1,[1,2,3]],[-1,{\"x\":4}],[10,{\"y\":5}]]"); } }; Index: ps/trunk/source/simulation2/components/tests/test_Position.h =================================================================== --- ps/trunk/source/simulation2/components/tests/test_Position.h (revision 24180) +++ ps/trunk/source/simulation2/components/tests/test_Position.h (revision 24181) @@ -1,232 +1,232 @@ /* Copyright (C) 2017 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "simulation2/system/ComponentTest.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpWaterManager.h" class MockWater : public ICmpWaterManager { public: DEFAULT_MOCK_COMPONENT() virtual entity_pos_t GetWaterLevel(entity_pos_t UNUSED(x), entity_pos_t UNUSED(z)) const { return entity_pos_t::FromInt(100); } virtual float GetExactWaterLevel(float UNUSED(x), float UNUSED(z)) const { return 100.f; } virtual void RecomputeWaterData() { } virtual void SetWaterLevel(entity_pos_t UNUSED(h)) { } }; class TestCmpPosition : public CxxTest::TestSuite { public: void setUp() { CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); } static CFixedVector3D fixedvec(int x, int y, int z) { return CFixedVector3D(fixed::FromInt(x), fixed::FromInt(y), fixed::FromInt(z)); } void test_basic() { - ComponentTestHelper test(g_ScriptRuntime); + ComponentTestHelper test(g_ScriptContext); MockTerrain terrain; test.AddMock(SYSTEM_ENTITY, IID_Terrain, terrain); ICmpPosition* cmp = test.Add(CID_Position, "upright23false"); // Defaults TS_ASSERT(!cmp->IsInWorld()); TS_ASSERT_EQUALS(cmp->GetHeightOffset(), entity_pos_t::FromInt(23)); TS_ASSERT_EQUALS(cmp->GetRotation(), fixedvec(0, 0, 0)); // Change height offset cmp->SetHeightOffset(entity_pos_t::FromInt(10)); TS_ASSERT_EQUALS(cmp->GetHeightOffset(), entity_pos_t::FromInt(10)); // Move out of world, while currently out of world cmp->MoveOutOfWorld(); TS_ASSERT(!cmp->IsInWorld()); // Jump into world cmp->JumpTo(entity_pos_t::FromInt(0), entity_pos_t::FromInt(0)); TS_ASSERT(cmp->IsInWorld()); // Move out of world, while currently in world cmp->MoveOutOfWorld(); TS_ASSERT(!cmp->IsInWorld()); // Move into world cmp->MoveTo(entity_pos_t::FromInt(100), entity_pos_t::FromInt(200)); TS_ASSERT(cmp->IsInWorld()); // Position computed from move plus terrain TS_ASSERT_EQUALS(cmp->GetPosition(), fixedvec(100, 60, 200)); // Interpolated position should be constant TS_ASSERT_EQUALS(cmp->GetInterpolatedTransform(0.0f).GetTranslation(), CVector3D(100, 60, 200)); TS_ASSERT_EQUALS(cmp->GetInterpolatedTransform(0.5f).GetTranslation(), CVector3D(100, 60, 200)); TS_ASSERT_EQUALS(cmp->GetInterpolatedTransform(1.0f).GetTranslation(), CVector3D(100, 60, 200)); // No TurnStart message, so this move doesn't affect the interpolation cmp->MoveTo(entity_pos_t::FromInt(0), entity_pos_t::FromInt(0)); // Move smoothly to new position cmp->MoveTo(entity_pos_t::FromInt(200), entity_pos_t::FromInt(0)); // Position computed from move plus terrain TS_ASSERT_EQUALS(cmp->GetPosition(), fixedvec(200, 60, 0)); // Interpolated position should vary, from original move into world to new move TS_ASSERT_EQUALS(cmp->GetInterpolatedTransform(0.0f).GetTranslation(), CVector3D(100, 60, 200)); TS_ASSERT_EQUALS(cmp->GetInterpolatedTransform(0.5f).GetTranslation(), CVector3D(150, 60, 100)); TS_ASSERT_EQUALS(cmp->GetInterpolatedTransform(1.0f).GetTranslation(), CVector3D(200, 60, 0)); // Latch new position for interpolation CMessageTurnStart msg; test.HandleMessage(cmp, msg, false); // Move smoothly to new position cmp->MoveTo(entity_pos_t::FromInt(400), entity_pos_t::FromInt(300)); TS_ASSERT_EQUALS(cmp->GetInterpolatedTransform(0.0f).GetTranslation(), CVector3D(200, 60, 0)); TS_ASSERT_EQUALS(cmp->GetInterpolatedTransform(0.5f).GetTranslation(), CVector3D(300, 60, 150)); TS_ASSERT_EQUALS(cmp->GetInterpolatedTransform(1.0f).GetTranslation(), CVector3D(400, 60, 300)); // Jump to new position cmp->JumpTo(entity_pos_t::FromInt(300), entity_pos_t::FromInt(100)); // Position computed from jump plus terrain TS_ASSERT_EQUALS(cmp->GetPosition(), fixedvec(300, 60, 100)); // Interpolated position should be constant after jump TS_ASSERT_EQUALS(cmp->GetInterpolatedTransform(0.0f).GetTranslation(), CVector3D(300, 60, 100)); TS_ASSERT_EQUALS(cmp->GetInterpolatedTransform(0.5f).GetTranslation(), CVector3D(300, 60, 100)); TS_ASSERT_EQUALS(cmp->GetInterpolatedTransform(1.0f).GetTranslation(), CVector3D(300, 60, 100)); // TODO: Test the rotation methods } void test_water() { - ComponentTestHelper test(g_ScriptRuntime); + ComponentTestHelper test(g_ScriptContext); MockTerrain terrain; test.AddMock(SYSTEM_ENTITY, IID_Terrain, terrain); MockWater water; test.AddMock(SYSTEM_ENTITY, IID_WaterManager, water); ICmpPosition* cmp = test.Add(CID_Position, "upright23true1"); // Move into the world, the fixed height uses the water level minus the float depth as a base cmp->JumpTo(entity_pos_t::FromInt(0), entity_pos_t::FromInt(0)); TS_ASSERT(cmp->IsInWorld()); TS_ASSERT(cmp->CanFloat()); TS_ASSERT_EQUALS(cmp->GetHeightOffset(), entity_pos_t::FromInt(23)); TS_ASSERT_EQUALS(cmp->GetHeightFixed(), entity_pos_t::FromInt(122)); // Change height offset, the fixed height changes too cmp->SetHeightOffset(entity_pos_t::FromInt(11)); TS_ASSERT_EQUALS(cmp->GetHeightOffset(), entity_pos_t::FromInt(11)); TS_ASSERT_EQUALS(cmp->GetHeightFixed(), entity_pos_t::FromInt(110)); TS_ASSERT_EQUALS(cmp->GetPosition(), fixedvec(0, 110, 0)); // Move cmp->MoveTo(entity_pos_t::FromInt(100), entity_pos_t::FromInt(200)); TS_ASSERT_EQUALS(cmp->GetInterpolatedTransform(0.0f).GetTranslation(), CVector3D(0, 122, 0)); TS_ASSERT_EQUALS(cmp->GetInterpolatedTransform(0.5f).GetTranslation(), CVector3D(50, 116, 100)); TS_ASSERT_EQUALS(cmp->GetInterpolatedTransform(1.0f).GetTranslation(), CVector3D(100, 110, 200)); // Change fixed height, the height offset changes too cmp->SetHeightFixed(entity_pos_t::FromInt(122)); TS_ASSERT_EQUALS(cmp->GetHeightFixed(), entity_pos_t::FromInt(122)); TS_ASSERT_EQUALS(cmp->GetHeightOffset(), entity_pos_t::FromInt(23)); TS_ASSERT_EQUALS(cmp->GetPosition(), fixedvec(100, 122, 200)); // The entity can't float anymore, the fixed height is computed from the terrain base cmp->SetFloating(false); TS_ASSERT(!cmp->CanFloat()); TS_ASSERT_EQUALS(cmp->GetHeightFixed(), entity_pos_t::FromInt(73)); TS_ASSERT_EQUALS(cmp->GetHeightOffset(), entity_pos_t::FromInt(23)); TS_ASSERT_EQUALS(cmp->GetPosition(), fixedvec(100, 73, 200)); // The entity can float again cmp->SetFloating(true); TS_ASSERT_EQUALS(cmp->GetHeightFixed(), entity_pos_t::FromInt(122)); TS_ASSERT_EQUALS(cmp->GetHeightOffset(), entity_pos_t::FromInt(23)); TS_ASSERT_EQUALS(cmp->GetPosition(), fixedvec(100, 122, 200)); // Use non relative height, entity will not follow terrain/water height. TS_ASSERT(cmp->IsHeightRelative()); cmp->SetHeightRelative(false); TS_ASSERT(!cmp->IsHeightRelative()); cmp->SetHeightOffset(entity_pos_t::FromInt(11)); TS_ASSERT_EQUALS(cmp->GetHeightOffset(), entity_pos_t::FromInt(11)); TS_ASSERT_EQUALS(cmp->GetHeightFixed(), entity_pos_t::FromInt(110)); TS_ASSERT_EQUALS(cmp->GetPosition(), fixedvec(100, 110, 200)); cmp->SetHeightFixed(entity_pos_t::FromInt(122)); TS_ASSERT_EQUALS(cmp->GetHeightFixed(), entity_pos_t::FromInt(122)); TS_ASSERT_EQUALS(cmp->GetHeightOffset(), entity_pos_t::FromInt(23)); TS_ASSERT_EQUALS(cmp->GetPosition(), fixedvec(100, 122, 200)); // The entity can't float anymore and height is not relative, fixed height doesn't change cmp->SetFloating(false); TS_ASSERT(!cmp->CanFloat()); TS_ASSERT_EQUALS(cmp->GetHeightFixed(), entity_pos_t::FromInt(122)); TS_ASSERT_EQUALS(cmp->GetHeightOffset(), entity_pos_t::FromInt(72)); TS_ASSERT_EQUALS(cmp->GetPosition(), fixedvec(100, 122, 200)); } void test_serialize() { - ComponentTestHelper test(g_ScriptRuntime); + ComponentTestHelper test(g_ScriptContext); MockTerrain terrain; test.AddMock(SYSTEM_ENTITY, IID_Terrain, terrain); ICmpPosition* cmp = test.Add(CID_Position, "upright5false"); test.Roundtrip(); cmp->SetHeightOffset(entity_pos_t::FromInt(20)); cmp->SetXZRotation(entity_angle_t::FromInt(1), entity_angle_t::FromInt(2)); cmp->SetYRotation(entity_angle_t::FromInt(3)); test.Roundtrip(); cmp->JumpTo(entity_pos_t::FromInt(10), entity_pos_t::FromInt(20)); cmp->MoveTo(entity_pos_t::FromInt(123), entity_pos_t::FromInt(456)); test.Roundtrip(); } }; Index: ps/trunk/source/simulation2/system/ComponentManager.cpp =================================================================== --- ps/trunk/source/simulation2/system/ComponentManager.cpp (revision 24180) +++ ps/trunk/source/simulation2/system/ComponentManager.cpp (revision 24181) @@ -1,1175 +1,1175 @@ /* Copyright (C) 2020 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 "ComponentManager.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Profile.h" #include "ps/scripting/JSInterface_VFS.h" #include "simulation2/components/ICmpTemplateManager.h" #include "simulation2/MessageTypes.h" #include "simulation2/system/DynamicSubscription.h" #include "simulation2/system/IComponent.h" #include "simulation2/system/ParamNode.h" #include "simulation2/system/SimContext.h" /** * Used for script-only message types. */ class CMessageScripted : public CMessage { public: virtual int GetType() const { return mtid; } virtual const char* GetScriptHandlerName() const { return handlerName.c_str(); } virtual const char* GetScriptGlobalHandlerName() const { return globalHandlerName.c_str(); } virtual JS::Value ToJSVal(const ScriptInterface& UNUSED(scriptInterface)) const { return msg.get(); } CMessageScripted(const ScriptInterface& scriptInterface, int mtid, const std::string& name, JS::HandleValue msg) : mtid(mtid), handlerName("On" + name), globalHandlerName("OnGlobal" + name), msg(scriptInterface.GetJSRuntime(), msg) { } int mtid; std::string handlerName; std::string globalHandlerName; JS::PersistentRootedValue msg; }; -CComponentManager::CComponentManager(CSimContext& context, shared_ptr rt, bool skipScriptFunctions) : +CComponentManager::CComponentManager(CSimContext& context, shared_ptr rt, bool skipScriptFunctions) : m_NextScriptComponentTypeId(CID__LastNative), m_ScriptInterface("Engine", "Simulation", rt), m_SimContext(context), m_CurrentlyHotloading(false) { context.SetComponentManager(this); m_ScriptInterface.SetCallbackData(static_cast (this)); m_ScriptInterface.ReplaceNondeterministicRNG(m_RNG); // For component script tests, the test system sets up its own scripted implementation of // these functions, so we skip registering them here in those cases if (!skipScriptFunctions) { JSI_VFS::RegisterScriptFunctions_Simulation(m_ScriptInterface); m_ScriptInterface.RegisterFunction ("RegisterComponentType"); m_ScriptInterface.RegisterFunction ("RegisterSystemComponentType"); m_ScriptInterface.RegisterFunction ("ReRegisterComponentType"); m_ScriptInterface.RegisterFunction ("RegisterInterface"); m_ScriptInterface.RegisterFunction ("RegisterMessageType"); m_ScriptInterface.RegisterFunction ("RegisterGlobal"); m_ScriptInterface.RegisterFunction ("QueryInterface"); m_ScriptInterface.RegisterFunction, int, CComponentManager::Script_GetEntitiesWithInterface> ("GetEntitiesWithInterface"); m_ScriptInterface.RegisterFunction, int, CComponentManager::Script_GetComponentsWithInterface> ("GetComponentsWithInterface"); m_ScriptInterface.RegisterFunction ("PostMessage"); m_ScriptInterface.RegisterFunction ("BroadcastMessage"); m_ScriptInterface.RegisterFunction ("AddEntity"); m_ScriptInterface.RegisterFunction ("AddLocalEntity"); m_ScriptInterface.RegisterFunction ("DestroyEntity"); m_ScriptInterface.RegisterFunction ("FlushDestroyedEntities"); } // Globalscripts may use VFS script functions m_ScriptInterface.LoadGlobalScripts(); // Define MT_*, IID_* as script globals, and store their names #define MESSAGE(name) m_ScriptInterface.SetGlobal("MT_" #name, (int)MT_##name); #define INTERFACE(name) \ m_ScriptInterface.SetGlobal("IID_" #name, (int)IID_##name); \ m_InterfaceIdsByName[#name] = IID_##name; #define COMPONENT(name) #include "simulation2/TypeList.h" #undef MESSAGE #undef INTERFACE #undef COMPONENT m_ScriptInterface.SetGlobal("INVALID_ENTITY", (int)INVALID_ENTITY); m_ScriptInterface.SetGlobal("INVALID_PLAYER", (int)INVALID_PLAYER); m_ScriptInterface.SetGlobal("SYSTEM_ENTITY", (int)SYSTEM_ENTITY); m_ComponentsByInterface.resize(IID__LastNative); ResetState(); } CComponentManager::~CComponentManager() { ResetState(); } void CComponentManager::LoadComponentTypes() { #define MESSAGE(name) \ RegisterMessageType(MT_##name, #name); #define INTERFACE(name) \ extern void RegisterComponentInterface_##name(ScriptInterface&); \ RegisterComponentInterface_##name(m_ScriptInterface); #define COMPONENT(name) \ extern void RegisterComponentType_##name(CComponentManager&); \ m_CurrentComponent = CID_##name; \ RegisterComponentType_##name(*this); #include "simulation2/TypeList.h" m_CurrentComponent = CID__Invalid; #undef MESSAGE #undef INTERFACE #undef COMPONENT } bool CComponentManager::LoadScript(const VfsPath& filename, bool hotload) { m_CurrentlyHotloading = hotload; CVFSFile file; PSRETURN loadOk = file.Load(g_VFS, filename); if (loadOk != PSRETURN_OK) // VFS will log the failed file and the reason return false; std::string content = file.DecodeUTF8(); // assume it's UTF-8 bool ok = m_ScriptInterface.LoadScript(filename, content); m_CurrentlyHotloading = false; return ok; } void CComponentManager::Script_RegisterComponentType_Common(ScriptInterface::CmptPrivate* pCmptPrivate, int iid, const std::string& cname, JS::HandleValue ctor, bool reRegister, bool systemComponent) { CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); ScriptInterface::Request rq(componentManager->m_ScriptInterface); // Find the C++ component that wraps the interface int cidWrapper = componentManager->GetScriptWrapper(iid); if (cidWrapper == CID__Invalid) { componentManager->m_ScriptInterface.ReportError("Invalid interface id"); return; } const ComponentType& ctWrapper = componentManager->m_ComponentTypesById[cidWrapper]; bool mustReloadComponents = false; // for hotloading ComponentTypeId cid = componentManager->LookupCID(cname); if (cid == CID__Invalid) { if (reRegister) { componentManager->m_ScriptInterface.ReportError(("ReRegistering component type that was not registered before '" + cname + "'").c_str()); return; } // Allocate a new cid number cid = componentManager->m_NextScriptComponentTypeId++; componentManager->m_ComponentTypeIdsByName[cname] = cid; if (systemComponent) componentManager->MarkScriptedComponentForSystemEntity(cid); } else { // Component type is already loaded, so do hotloading: if (!componentManager->m_CurrentlyHotloading && !reRegister) { componentManager->m_ScriptInterface.ReportError(("Registering component type with already-registered name '" + cname + "'").c_str()); return; } const ComponentType& ctPrevious = componentManager->m_ComponentTypesById[cid]; // We can only replace scripted component types, not native ones if (ctPrevious.type != CT_Script) { componentManager->m_ScriptInterface.ReportError(("Loading script component type with same name '" + cname + "' as native component").c_str()); return; } // We don't support changing the IID of a component type (it would require fiddling // around with m_ComponentsByInterface and being careful to guarantee uniqueness per entity) if (ctPrevious.iid != iid) { // ...though it only matters if any components exist with this type if (!componentManager->m_ComponentsByTypeId[cid].empty()) { componentManager->m_ScriptInterface.ReportError("Hotloading script component type mustn't change interface ID"); return; } } // Remove the old component type's message subscriptions std::map >::iterator it; for (it = componentManager->m_LocalMessageSubscriptions.begin(); it != componentManager->m_LocalMessageSubscriptions.end(); ++it) { std::vector& types = it->second; std::vector::iterator ctit = find(types.begin(), types.end(), cid); if (ctit != types.end()) types.erase(ctit); } for (it = componentManager->m_GlobalMessageSubscriptions.begin(); it != componentManager->m_GlobalMessageSubscriptions.end(); ++it) { std::vector& types = it->second; std::vector::iterator ctit = find(types.begin(), types.end(), cid); if (ctit != types.end()) types.erase(ctit); } mustReloadComponents = true; } JS::RootedValue protoVal(rq.cx); if (!componentManager->m_ScriptInterface.GetProperty(ctor, "prototype", &protoVal)) { componentManager->m_ScriptInterface.ReportError("Failed to get property 'prototype'"); return; } if (!protoVal.isObject()) { componentManager->m_ScriptInterface.ReportError("Component has no constructor"); return; } std::string schema = ""; if (componentManager->m_ScriptInterface.HasProperty(protoVal, "Schema")) componentManager->m_ScriptInterface.GetProperty(protoVal, "Schema", schema); // Construct a new ComponentType, using the wrapper's alloc functions ComponentType ct{ CT_Script, iid, ctWrapper.alloc, ctWrapper.dealloc, cname, schema, std::unique_ptr(new JS::PersistentRootedValue(rq.cx, ctor)) }; componentManager->m_ComponentTypesById[cid] = std::move(ct); componentManager->m_CurrentComponent = cid; // needed by Subscribe // Find all the ctor prototype's On* methods, and subscribe to the appropriate messages: std::vector methods; if (!componentManager->m_ScriptInterface.EnumeratePropertyNames(protoVal, false, methods)) { componentManager->m_ScriptInterface.ReportError("Failed to enumerate component properties."); return; } for (std::vector::const_iterator it = methods.begin(); it != methods.end(); ++it) { // TODO C++17: string_view if (strncmp((it->c_str()), "On", 2) != 0) continue; std::string name = (*it).substr(2); // strip the "On" prefix // Handle "OnGlobalFoo" functions specially bool isGlobal = false; if (strncmp(name.c_str(), "Global", 6) == 0) { isGlobal = true; name = name.substr(6); } std::map::const_iterator mit = componentManager->m_MessageTypeIdsByName.find(name); if (mit == componentManager->m_MessageTypeIdsByName.end()) { componentManager->m_ScriptInterface.ReportError(("Registered component has unrecognized '" + *it + "' message handler method").c_str()); return; } if (isGlobal) componentManager->SubscribeGloballyToMessageType(mit->second); else componentManager->SubscribeToMessageType(mit->second); } componentManager->m_CurrentComponent = CID__Invalid; if (mustReloadComponents) { // For every script component with this cid, we need to switch its // prototype from the old constructor's prototype property to the new one's const std::map& comps = componentManager->m_ComponentsByTypeId[cid]; std::map::const_iterator eit = comps.begin(); for (; eit != comps.end(); ++eit) { JS::RootedValue instance(rq.cx, eit->second->GetJSInstance()); if (!instance.isNull()) componentManager->m_ScriptInterface.SetPrototype(instance, protoVal); } } } void CComponentManager::Script_RegisterComponentType(ScriptInterface::CmptPrivate* pCmptPrivate, int iid, const std::string& cname, JS::HandleValue ctor) { CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); componentManager->Script_RegisterComponentType_Common(pCmptPrivate, iid, cname, ctor, false, false); componentManager->m_ScriptInterface.SetGlobal(cname.c_str(), ctor, componentManager->m_CurrentlyHotloading); } void CComponentManager::Script_RegisterSystemComponentType(ScriptInterface::CmptPrivate* pCmptPrivate, int iid, const std::string& cname, JS::HandleValue ctor) { CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); componentManager->Script_RegisterComponentType_Common(pCmptPrivate, iid, cname, ctor, false, true); componentManager->m_ScriptInterface.SetGlobal(cname.c_str(), ctor, componentManager->m_CurrentlyHotloading); } void CComponentManager::Script_ReRegisterComponentType(ScriptInterface::CmptPrivate* pCmptPrivate, int iid, const std::string& cname, JS::HandleValue ctor) { Script_RegisterComponentType_Common(pCmptPrivate, iid, cname, ctor, true, false); } void CComponentManager::Script_RegisterInterface(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& name) { CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); std::map::iterator it = componentManager->m_InterfaceIdsByName.find(name); if (it != componentManager->m_InterfaceIdsByName.end()) { // Redefinitions are fine (and just get ignored) when hotloading; otherwise // they're probably unintentional and should be reported if (!componentManager->m_CurrentlyHotloading) componentManager->m_ScriptInterface.ReportError(("Registering interface with already-registered name '" + name + "'").c_str()); return; } // IIDs start at 1, so size+1 is the next unused one size_t id = componentManager->m_InterfaceIdsByName.size() + 1; componentManager->m_InterfaceIdsByName[name] = (InterfaceId)id; componentManager->m_ComponentsByInterface.resize(id+1); // add one so we can index by InterfaceId componentManager->m_ScriptInterface.SetGlobal(("IID_" + name).c_str(), (int)id); } void CComponentManager::Script_RegisterMessageType(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& name) { CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); std::map::iterator it = componentManager->m_MessageTypeIdsByName.find(name); if (it != componentManager->m_MessageTypeIdsByName.end()) { // Redefinitions are fine (and just get ignored) when hotloading; otherwise // they're probably unintentional and should be reported if (!componentManager->m_CurrentlyHotloading) componentManager->m_ScriptInterface.ReportError(("Registering message type with already-registered name '" + name + "'").c_str()); return; } // MTIDs start at 1, so size+1 is the next unused one size_t id = componentManager->m_MessageTypeIdsByName.size() + 1; componentManager->RegisterMessageType((MessageTypeId)id, name.c_str()); componentManager->m_ScriptInterface.SetGlobal(("MT_" + name).c_str(), (int)id); } void CComponentManager::Script_RegisterGlobal(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& name, JS::HandleValue value) { CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); componentManager->m_ScriptInterface.SetGlobal(name.c_str(), value, componentManager->m_CurrentlyHotloading); } IComponent* CComponentManager::Script_QueryInterface(ScriptInterface::CmptPrivate* pCmptPrivate, int ent, int iid) { CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); IComponent* component = componentManager->QueryInterface((entity_id_t)ent, iid); return component; } std::vector CComponentManager::Script_GetEntitiesWithInterface(ScriptInterface::CmptPrivate* pCmptPrivate, int iid) { CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); std::vector ret; const InterfaceListUnordered& ents = componentManager->GetEntitiesWithInterfaceUnordered(iid); for (InterfaceListUnordered::const_iterator it = ents.begin(); it != ents.end(); ++it) if (!ENTITY_IS_LOCAL(it->first)) ret.push_back(it->first); std::sort(ret.begin(), ret.end()); return ret; } std::vector CComponentManager::Script_GetComponentsWithInterface(ScriptInterface::CmptPrivate* pCmptPrivate, int iid) { CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); std::vector ret; InterfaceList ents = componentManager->GetEntitiesWithInterface(iid); for (InterfaceList::const_iterator it = ents.begin(); it != ents.end(); ++it) ret.push_back(it->second); // TODO: maybe we should exclude local entities return ret; } CMessage* CComponentManager::ConstructMessage(int mtid, JS::HandleValue data) { if (mtid == MT__Invalid || mtid > (int)m_MessageTypeIdsByName.size()) // (IDs start at 1 so use '>' here) LOGERROR("PostMessage with invalid message type ID '%d'", mtid); if (mtid < MT__LastNative) { return CMessageFromJSVal(mtid, m_ScriptInterface, data); } else { return new CMessageScripted(m_ScriptInterface, mtid, m_MessageTypeNamesById[mtid], data); } } void CComponentManager::Script_PostMessage(ScriptInterface::CmptPrivate* pCmptPrivate, int ent, int mtid, JS::HandleValue data) { CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); CMessage* msg = componentManager->ConstructMessage(mtid, data); if (!msg) return; // error componentManager->PostMessage(ent, *msg); delete msg; } void CComponentManager::Script_BroadcastMessage(ScriptInterface::CmptPrivate* pCmptPrivate, int mtid, JS::HandleValue data) { CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); CMessage* msg = componentManager->ConstructMessage(mtid, data); if (!msg) return; // error componentManager->BroadcastMessage(*msg); delete msg; } int CComponentManager::Script_AddEntity(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& templateName) { CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); std::wstring name(templateName.begin(), templateName.end()); // TODO: should validate the string to make sure it doesn't contain scary characters // that will let it access non-component-template files entity_id_t ent = componentManager->AddEntity(name, componentManager->AllocateNewEntity()); return (int)ent; } int CComponentManager::Script_AddLocalEntity(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& templateName) { CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); std::wstring name(templateName.begin(), templateName.end()); // TODO: should validate the string to make sure it doesn't contain scary characters // that will let it access non-component-template files entity_id_t ent = componentManager->AddEntity(name, componentManager->AllocateNewLocalEntity()); return (int)ent; } void CComponentManager::Script_DestroyEntity(ScriptInterface::CmptPrivate* pCmptPrivate, int ent) { CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); componentManager->DestroyComponentsSoon(ent); } void CComponentManager::Script_FlushDestroyedEntities(ScriptInterface::CmptPrivate *pCmptPrivate) { CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); componentManager->FlushDestroyedComponents(); } void CComponentManager::ResetState() { // Delete all dynamic message subscriptions m_DynamicMessageSubscriptionsNonsync.clear(); m_DynamicMessageSubscriptionsNonsyncByComponent.clear(); // Delete all IComponents std::map >::iterator iit = m_ComponentsByTypeId.begin(); for (; iit != m_ComponentsByTypeId.end(); ++iit) { std::map::iterator eit = iit->second.begin(); for (; eit != iit->second.end(); ++eit) { eit->second->Deinit(); m_ComponentTypesById[iit->first].dealloc(eit->second); } } std::vector >::iterator ifcit = m_ComponentsByInterface.begin(); for (; ifcit != m_ComponentsByInterface.end(); ++ifcit) ifcit->clear(); m_ComponentsByTypeId.clear(); // Delete all SEntityComponentCaches std::unordered_map::iterator ccit = m_ComponentCaches.begin(); for (; ccit != m_ComponentCaches.end(); ++ccit) free(ccit->second); m_ComponentCaches.clear(); m_SystemEntity = CEntityHandle(); m_DestructionQueue.clear(); // Reset IDs m_NextEntityId = SYSTEM_ENTITY + 1; m_NextLocalEntityId = FIRST_LOCAL_ENTITY; } void CComponentManager::SetRNGSeed(u32 seed) { m_RNG.seed(seed); } void CComponentManager::RegisterComponentType(InterfaceId iid, ComponentTypeId cid, AllocFunc alloc, DeallocFunc dealloc, const char* name, const std::string& schema) { ComponentType c{ CT_Native, iid, alloc, dealloc, name, schema, std::unique_ptr() }; m_ComponentTypesById.insert(std::make_pair(cid, std::move(c))); m_ComponentTypeIdsByName[name] = cid; } void CComponentManager::RegisterComponentTypeScriptWrapper(InterfaceId iid, ComponentTypeId cid, AllocFunc alloc, DeallocFunc dealloc, const char* name, const std::string& schema) { ComponentType c{ CT_ScriptWrapper, iid, alloc, dealloc, name, schema, std::unique_ptr() }; m_ComponentTypesById.insert(std::make_pair(cid, std::move(c))); m_ComponentTypeIdsByName[name] = cid; // TODO: merge with RegisterComponentType } void CComponentManager::MarkScriptedComponentForSystemEntity(CComponentManager::ComponentTypeId cid) { m_ScriptedSystemComponents.push_back(cid); } void CComponentManager::RegisterMessageType(MessageTypeId mtid, const char* name) { m_MessageTypeIdsByName[name] = mtid; m_MessageTypeNamesById[mtid] = name; } void CComponentManager::SubscribeToMessageType(MessageTypeId mtid) { // TODO: verify mtid ENSURE(m_CurrentComponent != CID__Invalid); std::vector& types = m_LocalMessageSubscriptions[mtid]; types.push_back(m_CurrentComponent); std::sort(types.begin(), types.end()); // TODO: just sort once at the end of LoadComponents } void CComponentManager::SubscribeGloballyToMessageType(MessageTypeId mtid) { // TODO: verify mtid ENSURE(m_CurrentComponent != CID__Invalid); std::vector& types = m_GlobalMessageSubscriptions[mtid]; types.push_back(m_CurrentComponent); std::sort(types.begin(), types.end()); // TODO: just sort once at the end of LoadComponents } void CComponentManager::FlattenDynamicSubscriptions() { std::map::iterator it; for (it = m_DynamicMessageSubscriptionsNonsync.begin(); it != m_DynamicMessageSubscriptionsNonsync.end(); ++it) { it->second.Flatten(); } } void CComponentManager::DynamicSubscriptionNonsync(MessageTypeId mtid, IComponent* component, bool enable) { if (enable) { bool newlyInserted = m_DynamicMessageSubscriptionsNonsyncByComponent[component].insert(mtid).second; if (newlyInserted) m_DynamicMessageSubscriptionsNonsync[mtid].Add(component); } else { size_t numRemoved = m_DynamicMessageSubscriptionsNonsyncByComponent[component].erase(mtid); if (numRemoved) m_DynamicMessageSubscriptionsNonsync[mtid].Remove(component); } } void CComponentManager::RemoveComponentDynamicSubscriptions(IComponent* component) { std::map >::iterator it = m_DynamicMessageSubscriptionsNonsyncByComponent.find(component); if (it == m_DynamicMessageSubscriptionsNonsyncByComponent.end()) return; std::set::iterator mtit; for (mtit = it->second.begin(); mtit != it->second.end(); ++mtit) { m_DynamicMessageSubscriptionsNonsync[*mtit].Remove(component); // Need to flatten the subscription lists immediately to avoid dangling IComponent* references m_DynamicMessageSubscriptionsNonsync[*mtit].Flatten(); } m_DynamicMessageSubscriptionsNonsyncByComponent.erase(it); } CComponentManager::ComponentTypeId CComponentManager::LookupCID(const std::string& cname) const { std::map::const_iterator it = m_ComponentTypeIdsByName.find(cname); if (it == m_ComponentTypeIdsByName.end()) return CID__Invalid; return it->second; } std::string CComponentManager::LookupComponentTypeName(ComponentTypeId cid) const { std::map::const_iterator it = m_ComponentTypesById.find(cid); if (it == m_ComponentTypesById.end()) return ""; return it->second.name; } CComponentManager::ComponentTypeId CComponentManager::GetScriptWrapper(InterfaceId iid) { if (iid >= IID__LastNative && iid <= (int)m_InterfaceIdsByName.size()) // use <= since IDs start at 1 return CID_UnknownScript; std::map::const_iterator it = m_ComponentTypesById.begin(); for (; it != m_ComponentTypesById.end(); ++it) if (it->second.iid == iid && it->second.type == CT_ScriptWrapper) return it->first; std::map::const_iterator iiit = m_InterfaceIdsByName.begin(); for (; iiit != m_InterfaceIdsByName.end(); ++iiit) if (iiit->second == iid) { LOGERROR("No script wrapper found for interface id %d '%s'", iid, iiit->first.c_str()); return CID__Invalid; } LOGERROR("No script wrapper found for interface id %d", iid); return CID__Invalid; } entity_id_t CComponentManager::AllocateNewEntity() { entity_id_t id = m_NextEntityId++; // TODO: check for overflow return id; } entity_id_t CComponentManager::AllocateNewLocalEntity() { entity_id_t id = m_NextLocalEntityId++; // TODO: check for overflow return id; } entity_id_t CComponentManager::AllocateNewEntity(entity_id_t preferredId) { // TODO: ensure this ID hasn't been allocated before // (this might occur with broken map files) // Trying to actually add two entities with the same id will fail in AddEntitiy entity_id_t id = preferredId; // Ensure this ID won't be allocated again if (id >= m_NextEntityId) m_NextEntityId = id+1; // TODO: check for overflow return id; } bool CComponentManager::AddComponent(CEntityHandle ent, ComponentTypeId cid, const CParamNode& paramNode) { IComponent* component = ConstructComponent(ent, cid); if (!component) return false; component->Init(paramNode); return true; } void CComponentManager::AddSystemComponents(bool skipScriptedComponents, bool skipAI) { CParamNode noParam; AddComponent(m_SystemEntity, CID_TemplateManager, noParam); AddComponent(m_SystemEntity, CID_CinemaManager, noParam); AddComponent(m_SystemEntity, CID_CommandQueue, noParam); AddComponent(m_SystemEntity, CID_ObstructionManager, noParam); AddComponent(m_SystemEntity, CID_ParticleManager, noParam); AddComponent(m_SystemEntity, CID_Pathfinder, noParam); AddComponent(m_SystemEntity, CID_ProjectileManager, noParam); AddComponent(m_SystemEntity, CID_RangeManager, noParam); AddComponent(m_SystemEntity, CID_SoundManager, noParam); AddComponent(m_SystemEntity, CID_Terrain, noParam); AddComponent(m_SystemEntity, CID_TerritoryManager, noParam); AddComponent(m_SystemEntity, CID_UnitRenderer, noParam); AddComponent(m_SystemEntity, CID_WaterManager, noParam); // Add scripted system components: if (!skipScriptedComponents) { for (uint32_t i = 0; i < m_ScriptedSystemComponents.size(); ++i) AddComponent(m_SystemEntity, m_ScriptedSystemComponents[i], noParam); if (!skipAI) AddComponent(m_SystemEntity, CID_AIManager, noParam); } } IComponent* CComponentManager::ConstructComponent(CEntityHandle ent, ComponentTypeId cid) { ScriptInterface::Request rq(m_ScriptInterface); std::map::const_iterator it = m_ComponentTypesById.find(cid); if (it == m_ComponentTypesById.end()) { LOGERROR("Invalid component id %d", cid); return NULL; } const ComponentType& ct = it->second; ENSURE((size_t)ct.iid < m_ComponentsByInterface.size()); std::unordered_map& emap1 = m_ComponentsByInterface[ct.iid]; if (emap1.find(ent.GetId()) != emap1.end()) { LOGERROR("Multiple components for interface %d", ct.iid); return NULL; } std::map& emap2 = m_ComponentsByTypeId[cid]; // If this is a scripted component, construct the appropriate JS object first JS::RootedValue obj(rq.cx); if (ct.type == CT_Script) { m_ScriptInterface.CallConstructor(*ct.ctor, JS::HandleValueArray::empty(), &obj); if (obj.isNull()) { LOGERROR("Script component constructor failed"); return NULL; } } // Construct the new component IComponent* component = ct.alloc(m_ScriptInterface, obj); ENSURE(component); component->SetEntityHandle(ent); component->SetSimContext(m_SimContext); // Store a reference to the new component emap1.insert(std::make_pair(ent.GetId(), component)); emap2.insert(std::make_pair(ent.GetId(), component)); // TODO: We need to more careful about this - if an entity is constructed by a component // while we're iterating over all components, this will invalidate the iterators and everything // will break. // We probably need some kind of delayed addition, so they get pushed onto a queue and then // inserted into the world later on. (Be careful about immediation deletion in that case, too.) SEntityComponentCache* cache = ent.GetComponentCache(); ENSURE(cache != NULL && ct.iid < (int)cache->numInterfaces && cache->interfaces[ct.iid] == NULL); cache->interfaces[ct.iid] = component; return component; } void CComponentManager::AddMockComponent(CEntityHandle ent, InterfaceId iid, IComponent& component) { // Just add it into the by-interface map, not the by-component-type map, // so it won't be considered for messages or deletion etc std::unordered_map& emap1 = m_ComponentsByInterface.at(iid); if (emap1.find(ent.GetId()) != emap1.end()) debug_warn(L"Multiple components for interface"); emap1.insert(std::make_pair(ent.GetId(), &component)); SEntityComponentCache* cache = ent.GetComponentCache(); ENSURE(cache != NULL && iid < (int)cache->numInterfaces && cache->interfaces[iid] == NULL); cache->interfaces[iid] = &component; } CEntityHandle CComponentManager::AllocateEntityHandle(entity_id_t ent) { ENSURE(!EntityExists(ent)); // Interface IDs start at 1, and SEntityComponentCache is defined with a 1-sized array, // so we need space for an extra m_InterfaceIdsByName.size() items SEntityComponentCache* cache = (SEntityComponentCache*)calloc(1, sizeof(SEntityComponentCache) + sizeof(IComponent*) * m_InterfaceIdsByName.size()); ENSURE(cache != NULL); cache->numInterfaces = m_InterfaceIdsByName.size() + 1; m_ComponentCaches[ent] = cache; return CEntityHandle(ent, cache); } CEntityHandle CComponentManager::LookupEntityHandle(entity_id_t ent, bool allowCreate) { std::unordered_map::iterator it; it = m_ComponentCaches.find(ent); if (it == m_ComponentCaches.end()) { if (allowCreate) return AllocateEntityHandle(ent); else return CEntityHandle(ent, NULL); } else return CEntityHandle(ent, it->second); } void CComponentManager::InitSystemEntity() { ENSURE(m_SystemEntity.GetId() == INVALID_ENTITY); m_SystemEntity = AllocateEntityHandle(SYSTEM_ENTITY); m_SimContext.SetSystemEntity(m_SystemEntity); } entity_id_t CComponentManager::AddEntity(const std::wstring& templateName, entity_id_t ent) { ICmpTemplateManager *cmpTemplateManager = static_cast (QueryInterface(SYSTEM_ENTITY, IID_TemplateManager)); if (!cmpTemplateManager) { debug_warn(L"No ICmpTemplateManager loaded"); return INVALID_ENTITY; } const CParamNode* tmpl = cmpTemplateManager->LoadTemplate(ent, utf8_from_wstring(templateName)); if (!tmpl) return INVALID_ENTITY; // LoadTemplate will have reported the error // This also ensures that ent does not exist CEntityHandle handle = AllocateEntityHandle(ent); // Construct a component for each child of the root element const CParamNode::ChildrenMap& tmplChilds = tmpl->GetChildren(); for (CParamNode::ChildrenMap::const_iterator it = tmplChilds.begin(); it != tmplChilds.end(); ++it) { // Ignore attributes on the root element if (it->first.length() && it->first[0] == '@') continue; CComponentManager::ComponentTypeId cid = LookupCID(it->first); if (cid == CID__Invalid) { LOGERROR("Unrecognized component type name '%s' in entity template '%s'", it->first, utf8_from_wstring(templateName)); return INVALID_ENTITY; } if (!AddComponent(handle, cid, it->second)) { LOGERROR("Failed to construct component type name '%s' in entity template '%s'", it->first, utf8_from_wstring(templateName)); return INVALID_ENTITY; } // TODO: maybe we should delete already-constructed components if one of them fails? } CMessageCreate msg(ent); PostMessage(ent, msg); return ent; } bool CComponentManager::EntityExists(entity_id_t ent) const { return m_ComponentCaches.find(ent) != m_ComponentCaches.end(); } void CComponentManager::DestroyComponentsSoon(entity_id_t ent) { m_DestructionQueue.push_back(ent); } void CComponentManager::FlushDestroyedComponents() { PROFILE2("Flush Destroyed Components"); while (!m_DestructionQueue.empty()) { // Make a copy of the destruction queue, so that the iterators won't be invalidated if the // CMessageDestroy handlers try to destroy more entities themselves std::vector queue; queue.swap(m_DestructionQueue); for (std::vector::iterator it = queue.begin(); it != queue.end(); ++it) { entity_id_t ent = *it; // Do nothing if invalid, destroyed, etc. if (!EntityExists(ent)) continue; CEntityHandle handle = LookupEntityHandle(ent); CMessageDestroy msg(ent); PostMessage(ent, msg); // Flatten all the dynamic subscriptions to ensure there are no dangling // references in the 'removed' lists to components we're going to delete // Some components may have dynamically unsubscribed following the Destroy message FlattenDynamicSubscriptions(); // Destroy the components, and remove from m_ComponentsByTypeId: std::map >::iterator iit = m_ComponentsByTypeId.begin(); for (; iit != m_ComponentsByTypeId.end(); ++iit) { std::map::iterator eit = iit->second.find(ent); if (eit != iit->second.end()) { eit->second->Deinit(); RemoveComponentDynamicSubscriptions(eit->second); m_ComponentTypesById[iit->first].dealloc(eit->second); iit->second.erase(ent); handle.GetComponentCache()->interfaces[m_ComponentTypesById[iit->first].iid] = NULL; } } free(handle.GetComponentCache()); m_ComponentCaches.erase(ent); // Remove from m_ComponentsByInterface std::vector >::iterator ifcit = m_ComponentsByInterface.begin(); for (; ifcit != m_ComponentsByInterface.end(); ++ifcit) { ifcit->erase(ent); } } } } IComponent* CComponentManager::QueryInterface(entity_id_t ent, InterfaceId iid) const { if ((size_t)iid >= m_ComponentsByInterface.size()) { // Invalid iid return NULL; } std::unordered_map::const_iterator eit = m_ComponentsByInterface[iid].find(ent); if (eit == m_ComponentsByInterface[iid].end()) { // This entity doesn't implement this interface return NULL; } return eit->second; } CComponentManager::InterfaceList CComponentManager::GetEntitiesWithInterface(InterfaceId iid) const { std::vector > ret; if ((size_t)iid >= m_ComponentsByInterface.size()) { // Invalid iid return ret; } ret.reserve(m_ComponentsByInterface[iid].size()); std::unordered_map::const_iterator it = m_ComponentsByInterface[iid].begin(); for (; it != m_ComponentsByInterface[iid].end(); ++it) ret.push_back(*it); std::sort(ret.begin(), ret.end()); // lexicographic pair comparison means this'll sort by entity ID return ret; } static CComponentManager::InterfaceListUnordered g_EmptyEntityMap; const CComponentManager::InterfaceListUnordered& CComponentManager::GetEntitiesWithInterfaceUnordered(InterfaceId iid) const { if ((size_t)iid >= m_ComponentsByInterface.size()) { // Invalid iid return g_EmptyEntityMap; } return m_ComponentsByInterface[iid]; } void CComponentManager::PostMessage(entity_id_t ent, const CMessage& msg) { PROFILE2_IFSPIKE("Post Message", 0.0005); PROFILE2_ATTR("%s", msg.GetScriptHandlerName()); // Send the message to components of ent, that subscribed locally to this message std::map >::const_iterator it; it = m_LocalMessageSubscriptions.find(msg.GetType()); if (it != m_LocalMessageSubscriptions.end()) { std::vector::const_iterator ctit = it->second.begin(); for (; ctit != it->second.end(); ++ctit) { // Find the component instances of this type (if any) std::map >::const_iterator emap = m_ComponentsByTypeId.find(*ctit); if (emap == m_ComponentsByTypeId.end()) continue; // Send the message to all of them std::map::const_iterator eit = emap->second.find(ent); if (eit != emap->second.end()) eit->second->HandleMessage(msg, false); } } SendGlobalMessage(ent, msg); } void CComponentManager::BroadcastMessage(const CMessage& msg) { // Send the message to components of all entities that subscribed locally to this message std::map >::const_iterator it; it = m_LocalMessageSubscriptions.find(msg.GetType()); if (it != m_LocalMessageSubscriptions.end()) { std::vector::const_iterator ctit = it->second.begin(); for (; ctit != it->second.end(); ++ctit) { // Find the component instances of this type (if any) std::map >::const_iterator emap = m_ComponentsByTypeId.find(*ctit); if (emap == m_ComponentsByTypeId.end()) continue; // Send the message to all of them std::map::const_iterator eit = emap->second.begin(); for (; eit != emap->second.end(); ++eit) eit->second->HandleMessage(msg, false); } } SendGlobalMessage(INVALID_ENTITY, msg); } void CComponentManager::SendGlobalMessage(entity_id_t ent, const CMessage& msg) { PROFILE2_IFSPIKE("SendGlobalMessage", 0.001); PROFILE2_ATTR("%s", msg.GetScriptHandlerName()); // (Common functionality for PostMessage and BroadcastMessage) // Send the message to components of all entities that subscribed globally to this message std::map >::const_iterator it; it = m_GlobalMessageSubscriptions.find(msg.GetType()); if (it != m_GlobalMessageSubscriptions.end()) { std::vector::const_iterator ctit = it->second.begin(); for (; ctit != it->second.end(); ++ctit) { // Special case: Messages for local entities shouldn't be sent to script // components that subscribed globally, so that we don't have to worry about // them accidentally picking up non-network-synchronised data. if (ENTITY_IS_LOCAL(ent)) { std::map::const_iterator it = m_ComponentTypesById.find(*ctit); if (it != m_ComponentTypesById.end() && it->second.type == CT_Script) continue; } // Find the component instances of this type (if any) std::map >::const_iterator emap = m_ComponentsByTypeId.find(*ctit); if (emap == m_ComponentsByTypeId.end()) continue; // Send the message to all of them std::map::const_iterator eit = emap->second.begin(); for (; eit != emap->second.end(); ++eit) eit->second->HandleMessage(msg, true); } } // Send the message to component instances that dynamically subscribed to this message std::map::iterator dit = m_DynamicMessageSubscriptionsNonsync.find(msg.GetType()); if (dit != m_DynamicMessageSubscriptionsNonsync.end()) { dit->second.Flatten(); const std::vector& dynamic = dit->second.GetComponents(); for (size_t i = 0; i < dynamic.size(); i++) dynamic[i]->HandleMessage(msg, false); } } std::string CComponentManager::GenerateSchema() const { std::string schema = "" "" "" "" "" "0" "" "" "0" "" "" "" "" "" "" "" "" "" "" "" "" ""; std::map > interfaceComponentTypes; std::vector componentTypes; for (std::map::const_iterator it = m_ComponentTypesById.begin(); it != m_ComponentTypesById.end(); ++it) { schema += "" "" "" + it->second.schema + "" "" ""; interfaceComponentTypes[it->second.iid].push_back(it->second.name); componentTypes.push_back(it->second.name); } // Declare the implementation of each interface, for documentation for (std::map::const_iterator it = m_InterfaceIdsByName.begin(); it != m_InterfaceIdsByName.end(); ++it) { schema += ""; std::vector& cts = interfaceComponentTypes[it->second]; for (size_t i = 0; i < cts.size(); ++i) schema += ""; schema += ""; } // List all the component types, in alphabetical order (to match the reordering performed by CParamNode). // (We do it this way, rather than ing all the interface definitions (which would additionally perform // a check that we don't use multiple component types of the same interface in one file), because libxml2 gives // useless error messages in the latter case; this way lets it report the real error.) std::sort(componentTypes.begin(), componentTypes.end()); schema += "" "" ""; for (std::vector::const_iterator it = componentTypes.begin(); it != componentTypes.end(); ++it) schema += ""; schema += "" ""; schema += ""; return schema; } Index: ps/trunk/source/simulation2/tests/test_CmpTemplateManager.h =================================================================== --- ps/trunk/source/simulation2/tests/test_CmpTemplateManager.h (revision 24180) +++ ps/trunk/source/simulation2/tests/test_CmpTemplateManager.h (revision 24181) @@ -1,257 +1,257 @@ /* Copyright (C) 2020 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")); CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); } void test_LoadTemplate() { CSimContext context; - CComponentManager man(context, g_ScriptRuntime); + 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_ScriptRuntime); + 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(); ScriptInterface::Request 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_ScriptRuntime); + 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_ScriptRuntime); + 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")); CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); } // This just attempts loading every public entity, to check there's no validation errors void test_load_all_DISABLED() // disabled since it's a bit slow and noisy { CTerrain dummy; - CSimulation2 sim(NULL, g_ScriptRuntime, &dummy); + 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 24180) +++ ps/trunk/source/simulation2/tests/test_Serializer.h (revision 24181) @@ -1,883 +1,883 @@ /* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "lib/self_test.h" #include "simulation2/serialization/DebugSerializer.h" #include "simulation2/serialization/HashSerializer.h" #include "simulation2/serialization/StdSerializer.h" #include "simulation2/serialization/StdDeserializer.h" #include "scriptinterface/ScriptInterface.h" #include "graphics/MapReader.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureManager.h" #include "lib/timer.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Loader.h" #include "ps/XML/Xeromyces.h" #include "simulation2/Simulation2.h" #include "callgrind.h" #include #define TS_ASSERT_STREAM(stream, len, buffer) \ TS_ASSERT_EQUALS(stream.str().length(), (size_t)len); \ TS_ASSERT_SAME_DATA(stream.str().data(), buffer, len) #define TSM_ASSERT_STREAM(m, stream, len, buffer) \ TSM_ASSERT_EQUALS(m, stream.str().length(), (size_t)len); \ TSM_ASSERT_SAME_DATA(m, stream.str().data(), buffer, len) class TestSerializer : public CxxTest::TestSuite { public: void serialize_types(ISerializer& serialize) { serialize.NumberI8_Unbounded("i8", (signed char)-123); serialize.NumberU8_Unbounded("u8", (unsigned char)255); serialize.NumberI16_Unbounded("i16", -12345); serialize.NumberU16_Unbounded("u16", 56789); serialize.NumberI32_Unbounded("i32", -123); serialize.NumberU32_Unbounded("u32", (unsigned)-123); serialize.NumberFloat_Unbounded("float", 1e+30f); serialize.NumberDouble_Unbounded("double", 1e+300); serialize.NumberFixed_Unbounded("fixed", fixed::FromFloat(1234.5f)); serialize.Bool("t", true); serialize.Bool("f", false); serialize.StringASCII("string", "example", 0, 255); serialize.StringASCII("string 2", "example\"\\\"", 0, 255); serialize.StringASCII("string 3", "example\n\ntest", 0, 255); wchar_t testw[] = { 't', 0xEA, 's', 't', 0 }; serialize.String("string 4", testw, 0, 255); serialize.RawBytes("raw bytes", (const u8*)"\0\1\2\3\x0f\x10", 6); } void test_Debug_basic() { - ScriptInterface script("Test", "Test", g_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + 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_ScriptRuntime); + ScriptInterface script("Test", "Test", g_ScriptContext); ScriptInterface::Request 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::string source; TSM_ASSERT(msg, script.CallFunction(newobj, "toSource", source)); TS_ASSERT_STR_EQUALS(source, expected); } void test_script_basic() { helper_script_roundtrip("Object", "({'x': 123, 'y': [1, 1.5, '2', 'test', undefined, null, true, false]})", /* expected: */ "({x:123, y:[1, 1.5, \"2\", \"test\", (void 0), null, true, false]})", /* expected stream: */ 116, "\x03" // SCRIPT_TYPE_OBJECT "\x02\0\0\0" // num props "\x01\x01\0\0\0" "x" // "x" "\x05" // SCRIPT_TYPE_INT "\x7b\0\0\0" // 123 "\x01\x01\0\0\0" "y" // "y" "\x02" // SCRIPT_TYPE_ARRAY "\x08\0\0\0" // array length "\x08\0\0\0" // num props "\x01\x01\0\0\0" "0" // "0" "\x05" "\x01\0\0\0" // SCRIPT_TYPE_INT 1 "\x01\x01\0\0\0" "1" // "1" "\x06" "\0\0\0\0\0\0\xf8\x3f" // SCRIPT_TYPE_DOUBLE 1.5 "\x01\x01\0\0\0" "2" // "2" "\x04" "\x01\x01\0\0\0" "2" // SCRIPT_TYPE_STRING "2" "\x01\x01\0\0\0" "3" // "3" "\x04" "\x01\x04\0\0\0" "test" // SCRIPT_TYPE_STRING "test" "\x01\x01\0\0\0" "4" // "4" "\x00" // SCRIPT_TYPE_VOID "\x01\x01\0\0\0" "5" // "5" "\x01" // SCRIPT_TYPE_NULL "\x01\x01\0\0\0" "6" // "6" "\x07" "\x01" // SCRIPT_TYPE_BOOLEAN true "\x01\x01\0\0\0" "7" // "7" "\x07" "\x00", // SCRIPT_TYPE_BOOLEAN false /* expected debug: */ "script: {\n" " \"x\": 123,\n" " \"y\": [\n" " 1,\n" " 1.5,\n" " \"2\",\n" " \"test\",\n" " null,\n" " null,\n" " true,\n" " false\n" " ]\n" "}\n" ); } void test_script_unicode() { helper_script_roundtrip("unicode", "({" "'x': \"\\x01\\x80\\xff\\u0100\\ud7ff\", " "'y': \"\\ue000\\ufffd\"" "})", /* expected: */ "({" "x:\"\\x01\\x80\\xFF\\u0100\\uD7FF\", " "y:\"\\uE000\\uFFFD\"" "})"); // Disabled since we no longer do the UTF-8 conversion that rejects invalid characters // TS_ASSERT_THROWS(helper_script_roundtrip("invalid chars 1", "(\"\\ud7ff\\ud800\")", "..."), PSERROR_Serialize_InvalidCharInString); // TS_ASSERT_THROWS(helper_script_roundtrip("invalid chars 2", "(\"\\udfff\")", "..."), PSERROR_Serialize_InvalidCharInString); // TS_ASSERT_THROWS(helper_script_roundtrip("invalid chars 3", "(\"\\uffff\")", "..."), PSERROR_Serialize_InvalidCharInString); // TS_ASSERT_THROWS(helper_script_roundtrip("invalid chars 4", "(\"\\ud800\\udc00\")" /* U+10000 */, "..."), PSERROR_Serialize_InvalidCharInString); helper_script_roundtrip("unicode", "\"\\ud800\\uffff\"", "(new String(\"\\uD800\\uFFFF\"))"); } void test_script_objects() { helper_script_roundtrip("Number", "[1, new Number('2.0'), 3]", "[1, (new Number(2)), 3]"); helper_script_roundtrip("Number with props", "var n=new Number('2.0'); n.foo='bar'; n", "(new Number(2))"); helper_script_roundtrip("String", "['test1', new String('test2'), 'test3']", "[\"test1\", (new String(\"test2\")), \"test3\"]"); helper_script_roundtrip("String with props", "var s=new String('test'); s.foo='bar'; s", "(new String(\"test\"))"); helper_script_roundtrip("Boolean", "[new Boolean('true'), false]", "[(new Boolean(true)), false]"); helper_script_roundtrip("Boolean with props", "var b=new Boolean('true'); b.foo='bar'; b", "(new Boolean(true))"); } void test_script_objects_properties() { helper_script_roundtrip("Object with null in prop name", "({\"foo\\0bar\":1})", "({\'foo\\x00bar\':1})"); } void test_script_typed_arrays_simple() { helper_script_roundtrip("Int8Array", "var arr=new Int8Array(8);" "for(var i=0; iMount(L"", DataDir()/"mods"/"public", VFS_MOUNT_MUST_EXIST)); TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir()/"_testcache")); // Need some stuff for terrain movement costs: // (TODO: this ought to be independent of any graphics code) new CTerrainTextureManager; g_TexMan.LoadTerrainTextures(); CTerrain terrain; - CSimulation2 sim2(NULL, g_ScriptRuntime, &terrain); + CSimulation2 sim2(NULL, g_ScriptContext, &terrain); sim2.LoadDefaultScripts(); sim2.ResetState(); std::unique_ptr mapReader(new CMapReader()); LDR_BeginRegistering(); mapReader->LoadMap(L"maps/skirmishes/Greek Acropolis (2).pmp", sim2.GetScriptInterface().GetJSRuntime(), JS::UndefinedHandleValue, &terrain, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &sim2, &sim2.GetSimContext(), -1, false); LDR_EndRegistering(); TS_ASSERT_OK(LDR_NonprogressiveLoad()); sim2.Update(0); { std::stringstream str; std::string hash; sim2.SerializeState(str); sim2.ComputeStateHash(hash, false); debug_printf("\n"); debug_printf("# size = %d\n", (int)str.str().length()); debug_printf("# hash = "); for (size_t i = 0; i < hash.size(); ++i) debug_printf("%02x", (unsigned int)(u8)hash[i]); debug_printf("\n"); } double t = timer_Time(); CALLGRIND_START_INSTRUMENTATION; size_t reps = 128; for (size_t i = 0; i < reps; ++i) { std::string hash; sim2.ComputeStateHash(hash, false); } CALLGRIND_STOP_INSTRUMENTATION; t = timer_Time() - t; debug_printf("# time = %f (%f/%d)\n", t/reps, t, (int)reps); // Shut down the world delete &g_TexMan; g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); CXeromyces::Terminate(); } }; Index: ps/trunk/source/graphics/MapGenerator.cpp =================================================================== --- ps/trunk/source/graphics/MapGenerator.cpp (revision 24180) +++ ps/trunk/source/graphics/MapGenerator.cpp (revision 24181) @@ -1,437 +1,437 @@ /* Copyright (C) 2020 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 "MapGenerator.h" #include "graphics/MapIO.h" #include "graphics/Patch.h" #include "graphics/Terrain.h" #include "lib/external_libraries/libsdl.h" #include "lib/status.h" #include "lib/timer.h" #include "lib/file/vfs/vfs_path.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/FileIo.h" #include "ps/Profile.h" #include "ps/scripting/JSInterface_VFS.h" -#include "scriptinterface/ScriptRuntime.h" +#include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptConversions.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/helpers/MapEdgeTiles.h" #include #include // TODO: Maybe this should be optimized depending on the map size. -constexpr int RMS_RUNTIME_SIZE = 96 * 1024 * 1024; +constexpr int RMS_CONTEXT_SIZE = 96 * 1024 * 1024; extern bool IsQuitRequested(); static bool MapGeneratorInterruptCallback(JSContext* UNUSED(cx)) { // This may not use SDL_IsQuitRequested(), because it runs in a thread separate to SDL, see SDL_PumpEvents if (IsQuitRequested()) { LOGWARNING("Quit requested!"); return false; } return true; } CMapGeneratorWorker::CMapGeneratorWorker(ScriptInterface* scriptInterface) : m_ScriptInterface(scriptInterface) { // If something happens before we initialize, that's a failure m_Progress = -1; } CMapGeneratorWorker::~CMapGeneratorWorker() { // Wait for thread to end if (m_WorkerThread.joinable()) m_WorkerThread.join(); } void CMapGeneratorWorker::Initialize(const VfsPath& scriptFile, const std::string& settings) { std::lock_guard lock(m_WorkerMutex); // Set progress to positive value m_Progress = 1; m_ScriptPath = scriptFile; m_Settings = settings; // Launch the worker thread m_WorkerThread = std::thread(RunThread, this); } void* CMapGeneratorWorker::RunThread(CMapGeneratorWorker* self) { debug_SetThreadName("MapGenerator"); g_Profiler2.RegisterCurrentThread("MapGenerator"); - shared_ptr mapgenRuntime = ScriptRuntime::CreateRuntime(RMS_RUNTIME_SIZE); + shared_ptr mapgenContext = ScriptContext::CreateContext(RMS_CONTEXT_SIZE); // Enable the script to be aborted - JS_SetInterruptCallback(mapgenRuntime->GetJSRuntime(), MapGeneratorInterruptCallback); + JS_SetInterruptCallback(mapgenContext->GetJSRuntime(), MapGeneratorInterruptCallback); - self->m_ScriptInterface = new ScriptInterface("Engine", "MapGenerator", mapgenRuntime); + self->m_ScriptInterface = new ScriptInterface("Engine", "MapGenerator", mapgenContext); // Run map generation scripts if (!self->Run() || self->m_Progress > 0) { // Don't leave progress in an unknown state, if generator failed, set it to -1 std::lock_guard lock(self->m_WorkerMutex); self->m_Progress = -1; } SAFE_DELETE(self->m_ScriptInterface); // At this point the random map scripts are done running, so the thread has no further purpose // and can die. The data will be stored in m_MapData already if successful, or m_Progress // will contain an error value on failure. return NULL; } bool CMapGeneratorWorker::Run() { ScriptInterface::Request rq(m_ScriptInterface); // Parse settings JS::RootedValue settingsVal(rq.cx); if (!m_ScriptInterface->ParseJSON(m_Settings, &settingsVal) && settingsVal.isUndefined()) { LOGERROR("CMapGeneratorWorker::Run: Failed to parse settings"); return false; } // Prevent unintentional modifications to the settings object by random map scripts if (!m_ScriptInterface->FreezeObject(settingsVal, true)) { LOGERROR("CMapGeneratorWorker::Run: Failed to deepfreeze settings"); return false; } // Init RNG seed u32 seed = 0; if (!m_ScriptInterface->HasProperty(settingsVal, "Seed") || !m_ScriptInterface->GetProperty(settingsVal, "Seed", seed)) LOGWARNING("CMapGeneratorWorker::Run: No seed value specified - using 0"); InitScriptInterface(seed); RegisterScriptFunctions_MapGenerator(); // Copy settings to global variable JS::RootedValue global(rq.cx, rq.globalValue()); if (!m_ScriptInterface->SetProperty(global, "g_MapSettings", settingsVal, true, true)) { LOGERROR("CMapGeneratorWorker::Run: Failed to define g_MapSettings"); return false; } // Load RMS LOGMESSAGE("Loading RMS '%s'", m_ScriptPath.string8()); if (!m_ScriptInterface->LoadGlobalScriptFile(m_ScriptPath)) { LOGERROR("CMapGeneratorWorker::Run: Failed to load RMS '%s'", m_ScriptPath.string8()); return false; } return true; } void CMapGeneratorWorker::InitScriptInterface(const u32 seed) { m_ScriptInterface->SetCallbackData(static_cast(this)); m_ScriptInterface->ReplaceNondeterministicRNG(m_MapGenRNG); m_MapGenRNG.seed(seed); // VFS JSI_VFS::RegisterScriptFunctions_Maps(*m_ScriptInterface); // Globalscripts may use VFS script functions m_ScriptInterface->LoadGlobalScripts(); // File loading m_ScriptInterface->RegisterFunction("LoadLibrary"); m_ScriptInterface->RegisterFunction("LoadHeightmapImage"); m_ScriptInterface->RegisterFunction("LoadMapTerrain"); // Engine constants // Length of one tile of the terrain grid in metres. // Useful to transform footprint sizes to the tilegrid coordinate system. m_ScriptInterface->SetGlobal("TERRAIN_TILE_SIZE", static_cast(TERRAIN_TILE_SIZE)); // Number of impassable tiles at the map border m_ScriptInterface->SetGlobal("MAP_BORDER_WIDTH", static_cast(MAP_EDGE_TILES)); } void CMapGeneratorWorker::RegisterScriptFunctions_MapGenerator() { // Template functions m_ScriptInterface->RegisterFunction("GetTemplate"); m_ScriptInterface->RegisterFunction("TemplateExists"); m_ScriptInterface->RegisterFunction, std::string, bool, CMapGeneratorWorker::FindTemplates>("FindTemplates"); m_ScriptInterface->RegisterFunction, std::string, bool, CMapGeneratorWorker::FindActorTemplates>("FindActorTemplates"); // Progression and profiling m_ScriptInterface->RegisterFunction("SetProgress"); m_ScriptInterface->RegisterFunction("GetMicroseconds"); m_ScriptInterface->RegisterFunction("ExportMap"); } int CMapGeneratorWorker::GetProgress() { std::lock_guard lock(m_WorkerMutex); return m_Progress; } double CMapGeneratorWorker::GetMicroseconds(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) { return JS_Now(); } shared_ptr CMapGeneratorWorker::GetResults() { std::lock_guard lock(m_WorkerMutex); return m_MapData; } bool CMapGeneratorWorker::LoadLibrary(ScriptInterface::CmptPrivate* pCmptPrivate, const VfsPath& name) { CMapGeneratorWorker* self = static_cast(pCmptPrivate->pCBData); return self->LoadScripts(name); } void CMapGeneratorWorker::ExportMap(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue data) { CMapGeneratorWorker* self = static_cast(pCmptPrivate->pCBData); // Copy results std::lock_guard lock(self->m_WorkerMutex); self->m_MapData = self->m_ScriptInterface->WriteStructuredClone(data); self->m_Progress = 0; } void CMapGeneratorWorker::SetProgress(ScriptInterface::CmptPrivate* pCmptPrivate, int progress) { CMapGeneratorWorker* self = static_cast(pCmptPrivate->pCBData); // Copy data std::lock_guard lock(self->m_WorkerMutex); if (progress >= self->m_Progress) self->m_Progress = progress; else LOGWARNING("The random map script tried to reduce the loading progress from %d to %d", self->m_Progress, progress); } CParamNode CMapGeneratorWorker::GetTemplate(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& templateName) { CMapGeneratorWorker* self = static_cast(pCmptPrivate->pCBData); const CParamNode& templateRoot = self->m_TemplateLoader.GetTemplateFileData(templateName).GetChild("Entity"); if (!templateRoot.IsOk()) LOGERROR("Invalid template found for '%s'", templateName.c_str()); return templateRoot; } bool CMapGeneratorWorker::TemplateExists(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& templateName) { CMapGeneratorWorker* self = static_cast(pCmptPrivate->pCBData); return self->m_TemplateLoader.TemplateExists(templateName); } std::vector CMapGeneratorWorker::FindTemplates(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& path, bool includeSubdirectories) { CMapGeneratorWorker* self = static_cast(pCmptPrivate->pCBData); return self->m_TemplateLoader.FindTemplates(path, includeSubdirectories, SIMULATION_TEMPLATES); } std::vector CMapGeneratorWorker::FindActorTemplates(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& path, bool includeSubdirectories) { CMapGeneratorWorker* self = static_cast(pCmptPrivate->pCBData); return self->m_TemplateLoader.FindTemplates(path, includeSubdirectories, ACTOR_TEMPLATES); } bool CMapGeneratorWorker::LoadScripts(const VfsPath& libraryName) { // Ignore libraries that are already loaded if (m_LoadedLibraries.find(libraryName) != m_LoadedLibraries.end()) return true; // Mark this as loaded, to prevent it recursively loading itself m_LoadedLibraries.insert(libraryName); VfsPath path = VfsPath(L"maps/random/") / libraryName / VfsPath(); VfsPaths pathnames; // Load all scripts in mapgen directory Status ret = vfs::GetPathnames(g_VFS, path, L"*.js", pathnames); if (ret == INFO::OK) { for (const VfsPath& p : pathnames) { LOGMESSAGE("Loading map generator script '%s'", p.string8()); if (!m_ScriptInterface->LoadGlobalScriptFile(p)) { LOGERROR("CMapGeneratorWorker::LoadScripts: Failed to load script '%s'", p.string8()); return false; } } } else { // Some error reading directory wchar_t error[200]; LOGERROR("CMapGeneratorWorker::LoadScripts: Error reading scripts in directory '%s': %s", path.string8(), utf8_from_wstring(StatusDescription(ret, error, ARRAY_SIZE(error)))); return false; } return true; } JS::Value CMapGeneratorWorker::LoadHeightmap(ScriptInterface::CmptPrivate* pCmptPrivate, const VfsPath& filename) { std::vector heightmap; if (LoadHeightmapImageVfs(filename, heightmap) != INFO::OK) { LOGERROR("Could not load heightmap file '%s'", filename.string8()); return JS::UndefinedValue(); } CMapGeneratorWorker* self = static_cast(pCmptPrivate->pCBData); ScriptInterface::Request rq(self->m_ScriptInterface); JS::RootedValue returnValue(rq.cx); ToJSVal_vector(rq, &returnValue, heightmap); return returnValue; } // See CMapReader::UnpackTerrain, CMapReader::ParseTerrain for the reordering JS::Value CMapGeneratorWorker::LoadMapTerrain(ScriptInterface::CmptPrivate* pCmptPrivate, const VfsPath& filename) { CMapGeneratorWorker* self = static_cast(pCmptPrivate->pCBData); ScriptInterface::Request rq(self->m_ScriptInterface); if (!VfsFileExists(filename)) { self->m_ScriptInterface->ReportError( ("Terrain file \"" + filename.string8() + "\" does not exist!").c_str()); return JS::UndefinedValue(); } CFileUnpacker unpacker; unpacker.Read(filename, "PSMP"); if (unpacker.GetVersion() < CMapIO::FILE_READ_VERSION) { self->m_ScriptInterface->ReportError( ("Could not load terrain file \"" + filename.string8() + "\" too old version!").c_str()); return JS::UndefinedValue(); } // unpack size ssize_t patchesPerSide = (ssize_t)unpacker.UnpackSize(); size_t verticesPerSide = patchesPerSide * PATCH_SIZE + 1; // unpack heightmap std::vector heightmap; heightmap.resize(SQR(verticesPerSide)); unpacker.UnpackRaw(&heightmap[0], SQR(verticesPerSide) * sizeof(u16)); // unpack texture names size_t textureCount = unpacker.UnpackSize(); std::vector textureNames; textureNames.reserve(textureCount); for (size_t i = 0; i < textureCount; ++i) { CStr texturename; unpacker.UnpackString(texturename); textureNames.push_back(texturename); } // unpack texture IDs per tile ssize_t tilesPerSide = patchesPerSide * PATCH_SIZE; std::vector tiles; tiles.resize(size_t(SQR(tilesPerSide))); unpacker.UnpackRaw(&tiles[0], sizeof(CMapIO::STileDesc) * tiles.size()); // reorder by patches and store and save texture IDs per tile std::vector textureIDs; for (ssize_t x = 0; x < tilesPerSide; ++x) { size_t patchX = x / PATCH_SIZE; size_t offX = x % PATCH_SIZE; for (ssize_t y = 0; y < tilesPerSide; ++y) { size_t patchY = y / PATCH_SIZE; size_t offY = y % PATCH_SIZE; // m_Priority and m_Tex2Index unused textureIDs.push_back(tiles[(patchY * patchesPerSide + patchX) * SQR(PATCH_SIZE) + (offY * PATCH_SIZE + offX)].m_Tex1Index); } } JS::RootedValue returnValue(rq.cx); ScriptInterface::CreateObject( rq, &returnValue, "height", heightmap, "textureNames", textureNames, "textureIDs", textureIDs); return returnValue; } ////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////// CMapGenerator::CMapGenerator() : m_Worker(new CMapGeneratorWorker(nullptr)) { } CMapGenerator::~CMapGenerator() { delete m_Worker; } void CMapGenerator::GenerateMap(const VfsPath& scriptFile, const std::string& settings) { m_Worker->Initialize(scriptFile, settings); } int CMapGenerator::GetProgress() { return m_Worker->GetProgress(); } shared_ptr CMapGenerator::GetResults() { return m_Worker->GetResults(); } Index: ps/trunk/source/graphics/MapGenerator.h =================================================================== --- ps/trunk/source/graphics/MapGenerator.h (revision 24180) +++ ps/trunk/source/graphics/MapGenerator.h (revision 24181) @@ -1,248 +1,248 @@ /* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_MAPGENERATOR #define INCLUDED_MAPGENERATOR #include "lib/posix/posix_pthread.h" #include "ps/FileIo.h" #include "ps/TemplateLoader.h" #include "scriptinterface/ScriptInterface.h" #include #include #include #include #include class CMapGeneratorWorker; /** * Random map generator interface. Initialized by CMapReader and then checked * periodically during loading, until it's finished (progress value is 0). * * The actual work is performed by CMapGeneratorWorker in a separate thread. */ class CMapGenerator { NONCOPYABLE(CMapGenerator); public: CMapGenerator(); ~CMapGenerator(); /** * Start the map generator thread * * @param scriptFile The VFS path for the script, e.g. "maps/random/latium.js" * @param settings JSON string containing settings for the map generator */ void GenerateMap(const VfsPath& scriptFile, const std::string& settings); /** * Get status of the map generator thread * * @return Progress percentage 1-100 if active, 0 when finished, or -1 on error */ int GetProgress(); /** * Get random map data, according to this format: * http://trac.wildfiregames.com/wiki/Random_Map_Generator_Internals#Dataformat * * @return StructuredClone containing map data */ shared_ptr GetResults(); private: CMapGeneratorWorker* m_Worker; }; /** * Random map generator worker thread. * (This is run in a thread so that the GUI remains responsive while loading) * * Thread-safety: * - Initialize and constructor/destructor must be called from the main thread. * - ScriptInterface created and destroyed by thread - * - StructuredClone used to return JS map data - JS:Values can't be used across threads/runtimes. + * - StructuredClone used to return JS map data - JS:Values can't be used across threads/contexts. */ class CMapGeneratorWorker { public: CMapGeneratorWorker(ScriptInterface* scriptInterface); ~CMapGeneratorWorker(); /** * Start the map generator thread * * @param scriptFile The VFS path for the script, e.g. "maps/random/latium.js" * @param settings JSON string containing settings for the map generator */ void Initialize(const VfsPath& scriptFile, const std::string& settings); /** * Get status of the map generator thread * * @return Progress percentage 1-100 if active, 0 when finished, or -1 on error */ int GetProgress(); /** * Get random map data, according to this format: * http://trac.wildfiregames.com/wiki/Random_Map_Generator_Internals#Dataformat * * @return StructuredClone containing map data */ shared_ptr GetResults(); /** * Set initial seed, callback data. * Expose functions, globals and classes defined in this class relevant to the map and test scripts. */ void InitScriptInterface(const u32 seed); private: /** * Expose functions defined in this class that are relevant to mapscripts but not the tests. */ void RegisterScriptFunctions_MapGenerator(); /** * Load all scripts of the given library * * @param libraryName VfsPath specifying name of the library (subfolder of ../maps/random/) * @return true if all scripts ran successfully, false if there's an error */ bool LoadScripts(const VfsPath& libraryName); /** * Recursively load all script files in the given folder. */ static bool LoadLibrary(ScriptInterface::CmptPrivate* pCmptPrivate, const VfsPath& name); /** * Finalize map generation and pass results from the script to the engine. */ static void ExportMap(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue data); /** * Load an image file and return it as a height array. */ static JS::Value LoadHeightmap(ScriptInterface::CmptPrivate* pCmptPrivate, const VfsPath& src); /** * Load an Atlas terrain file (PMP) returning textures and heightmap. */ static JS::Value LoadMapTerrain(ScriptInterface::CmptPrivate* pCmptPrivate, const VfsPath& filename); /** * Sets the map generation progress, which is one of multiple stages determining the loading screen progress. */ static void SetProgress(ScriptInterface::CmptPrivate* pCmptPrivate, int progress); /** * Microseconds since the epoch. */ static double GetMicroseconds(ScriptInterface::CmptPrivate* pCmptPrivate); /** * Return the template data of the given template name. */ static CParamNode GetTemplate(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& templateName); /** * Check whether the given template exists. */ static bool TemplateExists(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& templateName); /** * Returns all template names of simulation entity templates. */ static std::vector FindTemplates(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& path, bool includeSubdirectories); /** * Returns all template names of actors. */ static std::vector FindActorTemplates(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& path, bool includeSubdirectories); /** * Perform map generation in an independent thread. */ static void* RunThread(CMapGeneratorWorker* self); /** * Perform the map generation. */ bool Run(); /** * Currently loaded script librarynames. */ std::set m_LoadedLibraries; /** * Result of the mapscript generation including terrain, entities and environment settings. */ shared_ptr m_MapData; /** * Deterministic random number generator. */ boost::rand48 m_MapGenRNG; /** * Current map generation progress. */ int m_Progress; /** * Provides the script context. */ ScriptInterface* m_ScriptInterface; /** * Map generation script to run. */ VfsPath m_ScriptPath; /** * Map and simulation settings chosen in the gamesetup stage. */ std::string m_Settings; /** * Backend to loading template data. */ CTemplateLoader m_TemplateLoader; /** * Holds the mapgeneration thread identifier. */ std::thread m_WorkerThread; /** * Avoids thread synchronization issues. */ std::mutex m_WorkerMutex; }; #endif //INCLUDED_MAPGENERATOR Index: ps/trunk/source/graphics/tests/test_LOSTexture.h =================================================================== --- ps/trunk/source/graphics/tests/test_LOSTexture.h (revision 24180) +++ ps/trunk/source/graphics/tests/test_LOSTexture.h (revision 24181) @@ -1,94 +1,94 @@ /* Copyright (C) 2020 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/LOSTexture.h" #include "lib/timer.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" #include "simulation2/helpers/Los.h" class TestLOSTexture : public CxxTest::TestSuite { public: void test_basic() { - CSimulation2 sim(NULL, g_ScriptRuntime, NULL); + CSimulation2 sim(NULL, g_ScriptContext, NULL); CLOSTexture tex(sim); const ssize_t size = 8; u32 inputData[size*size] = { 2, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2 }; Grid inputDataVec(size, size); for (u8 i = 0; i < size; ++i) for (u8 j = 0; j < size; ++j) inputDataVec.set(i, j, inputData[i + j * size]); // LosState::MASK should be cmpRanageManager->GetSharedLosMask(1), // but that would mean adding a huge mock component for this and it // should always be LosState::MASK for player 1 (as the other players are bit-shifted). CLosQuerier los((u32)LosState::MASK, inputDataVec, size); std::vector losData; size_t pitch; losData.resize(tex.GetBitmapSize(size, size, &pitch)); tex.GenerateBitmap(los, &losData[0], size, size, pitch); // for (size_t i = 0; i < losData.size(); ++i) // printf("%s %3d", i % (size_t)sqrt(losData.size()) ? "" : "\n", losData[i]); TS_ASSERT_EQUALS(losData[0], 104); } void test_perf_DISABLED() { - CSimulation2 sim(NULL, g_ScriptRuntime, NULL); + CSimulation2 sim(NULL, g_ScriptContext, NULL); CLOSTexture tex(sim); const ssize_t size = 257; Grid inputDataVec(size, size); // LosState::MASK should be cmpRanageManager->GetSharedLosMask(1), // but that would mean adding a huge mock component for this and it // should always be LosState::MASK for player 1 (as the other players are bit-shifted). CLosQuerier los((u32)LosState::MASK, inputDataVec, size); size_t reps = 128; double t = timer_Time(); for (size_t i = 0; i < reps; ++i) { std::vector losData; size_t pitch; losData.resize(tex.GetBitmapSize(size, size, &pitch)); tex.GenerateBitmap(los, &losData[0], size, size, pitch); } double dt = timer_Time() - t; printf("\n# %f secs\n", dt/reps); } }; Index: ps/trunk/source/graphics/tests/test_MapGenerator.h =================================================================== --- ps/trunk/source/graphics/tests/test_MapGenerator.h (revision 24180) +++ ps/trunk/source/graphics/tests/test_MapGenerator.h (revision 24181) @@ -1,60 +1,60 @@ /* Copyright (C) 2020 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 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_ScriptRuntime); + ScriptInterface scriptInterface("Engine", "MapGenerator", g_ScriptContext); ScriptTestSetup(scriptInterface); CMapGeneratorWorker worker(&scriptInterface); worker.InitScriptInterface(0); scriptInterface.LoadGlobalScriptFile(path); } } }; Index: ps/trunk/source/gui/CGUI.cpp =================================================================== --- ps/trunk/source/gui/CGUI.cpp (revision 24180) +++ ps/trunk/source/gui/CGUI.cpp (revision 24181) @@ -1,1280 +1,1280 @@ /* Copyright (C) 2020 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 "CGUI.h" #include "gui/GUIObjectTypes.h" #include "gui/IGUIScrollBar.h" #include "gui/Scripting/ScriptFunctions.h" #include "i18n/L10n.h" #include "lib/bits.h" #include "lib/input.h" #include "lib/sysdep/sysdep.h" #include "lib/timer.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/GameSetup/Config.h" #include "ps/Globals.h" #include "ps/Hotkey.h" #include "ps/Profile.h" #include "ps/Pyrogenesis.h" #include "ps/XML/Xeromyces.h" #include "renderer/Renderer.h" #include "scriptinterface/ScriptInterface.h" #include #include extern int g_yres; const double SELECT_DBLCLICK_RATE = 0.5; const u32 MAX_OBJECT_DEPTH = 100; // Max number of nesting for GUI includes. Used to detect recursive inclusion const CStr CGUI::EventNameLoad = "Load"; const CStr CGUI::EventNameTick = "Tick"; const CStr CGUI::EventNamePress = "Press"; const CStr CGUI::EventNameKeyDown = "KeyDown"; const CStr CGUI::EventNameRelease = "Release"; const CStr CGUI::EventNameMouseRightPress = "MouseRightPress"; const CStr CGUI::EventNameMouseLeftPress = "MouseLeftPress"; const CStr CGUI::EventNameMouseWheelDown = "MouseWheelDown"; const CStr CGUI::EventNameMouseWheelUp = "MouseWheelUp"; const CStr CGUI::EventNameMouseLeftDoubleClick = "MouseLeftDoubleClick"; const CStr CGUI::EventNameMouseLeftRelease = "MouseLeftRelease"; const CStr CGUI::EventNameMouseRightDoubleClick = "MouseRightDoubleClick"; const CStr CGUI::EventNameMouseRightRelease = "MouseRightRelease"; -CGUI::CGUI(const shared_ptr& runtime) +CGUI::CGUI(const shared_ptr& context) : m_BaseObject(*this), m_FocusedObject(nullptr), m_InternalNameNumber(0), m_MouseButtons(0) { - m_ScriptInterface.reset(new ScriptInterface("Engine", "GUIPage", runtime)); + m_ScriptInterface.reset(new ScriptInterface("Engine", "GUIPage", context)); m_ScriptInterface->SetCallbackData(this); GuiScriptingInit(*m_ScriptInterface); m_ScriptInterface->LoadGlobalScripts(); } CGUI::~CGUI() { for (const std::pair& p : m_pAllObjects) delete p.second; for (const std::pair& p : m_Sprites) delete p.second; } InReaction CGUI::HandleEvent(const SDL_Event_* ev) { InReaction ret = IN_PASS; if (ev->ev.type == SDL_HOTKEYDOWN || ev->ev.type == SDL_HOTKEYPRESS || ev->ev.type == SDL_HOTKEYUP) { const char* hotkey = static_cast(ev->ev.user.data1); const CStr& eventName = ev->ev.type == SDL_HOTKEYPRESS ? EventNamePress : ev->ev.type == SDL_HOTKEYDOWN ? EventNameKeyDown : EventNameRelease; if (m_GlobalHotkeys.find(hotkey) != m_GlobalHotkeys.end() && m_GlobalHotkeys[hotkey].find(eventName) != m_GlobalHotkeys[hotkey].end()) { ret = IN_HANDLED; ScriptInterface::Request rq(m_ScriptInterface); JS::RootedObject globalObj(rq.cx, rq.glob); JS::RootedValue result(rq.cx); JS_CallFunctionValue(rq.cx, globalObj, m_GlobalHotkeys[hotkey][eventName], JS::HandleValueArray::empty(), &result); } std::map >::iterator it = m_HotkeyObjects.find(hotkey); if (it != m_HotkeyObjects.end()) for (IGUIObject* const& obj : it->second) { if (ev->ev.type == SDL_HOTKEYPRESS) ret = obj->SendEvent(GUIM_PRESSED, EventNamePress); else if (ev->ev.type == SDL_HOTKEYDOWN) ret = obj->SendEvent(GUIM_KEYDOWN, EventNameKeyDown); else ret = obj->SendEvent(GUIM_RELEASED, EventNameRelease); } } else if (ev->ev.type == SDL_MOUSEMOTION) { // Yes the mouse position is stored as float to avoid // constant conversions when operating in a // float-based environment. m_MousePos = CPos((float)ev->ev.motion.x / g_GuiScale, (float)ev->ev.motion.y / g_GuiScale); SGUIMessage msg(GUIM_MOUSE_MOTION); m_BaseObject.RecurseObject(&IGUIObject::IsHiddenOrGhost, &IGUIObject::HandleMessage, msg); } // Update m_MouseButtons. (BUTTONUP is handled later.) else if (ev->ev.type == SDL_MOUSEBUTTONDOWN) { switch (ev->ev.button.button) { case SDL_BUTTON_LEFT: case SDL_BUTTON_RIGHT: case SDL_BUTTON_MIDDLE: m_MouseButtons |= Bit(ev->ev.button.button); break; default: break; } } // Update m_MousePos (for delayed mouse button events) CPos oldMousePos = m_MousePos; if (ev->ev.type == SDL_MOUSEBUTTONDOWN || ev->ev.type == SDL_MOUSEBUTTONUP) { m_MousePos = CPos((float)ev->ev.button.x / g_GuiScale, (float)ev->ev.button.y / g_GuiScale); } // Only one object can be hovered IGUIObject* pNearest = nullptr; // TODO Gee: (2004-09-08) Big TODO, don't do the below if the SDL_Event is something like a keypress! { PROFILE("mouse events"); // TODO Gee: Optimizations needed! // these two recursive function are quite overhead heavy. // pNearest will after this point at the hovered object, possibly nullptr pNearest = FindObjectUnderMouse(); // Now we'll call UpdateMouseOver on *all* objects, // we'll input the one hovered, and they will each // update their own data and send messages accordingly m_BaseObject.RecurseObject(&IGUIObject::IsHiddenOrGhost, &IGUIObject::UpdateMouseOver, static_cast(pNearest)); if (ev->ev.type == SDL_MOUSEBUTTONDOWN) { switch (ev->ev.button.button) { case SDL_BUTTON_LEFT: // Focus the clicked object (or focus none if nothing clicked on) SetFocusedObject(pNearest); if (pNearest) ret = pNearest->SendMouseEvent(GUIM_MOUSE_PRESS_LEFT, EventNameMouseLeftPress); break; case SDL_BUTTON_RIGHT: if (pNearest) ret = pNearest->SendMouseEvent(GUIM_MOUSE_PRESS_RIGHT, EventNameMouseRightPress); break; default: break; } } else if (ev->ev.type == SDL_MOUSEWHEEL && pNearest) { if (ev->ev.wheel.y < 0) ret = pNearest->SendMouseEvent(GUIM_MOUSE_WHEEL_DOWN, EventNameMouseWheelDown); else if (ev->ev.wheel.y > 0) ret = pNearest->SendMouseEvent(GUIM_MOUSE_WHEEL_UP, EventNameMouseWheelUp); } else if (ev->ev.type == SDL_MOUSEBUTTONUP) { switch (ev->ev.button.button) { case SDL_BUTTON_LEFT: if (pNearest) { double timeElapsed = timer_Time() - pNearest->m_LastClickTime[SDL_BUTTON_LEFT]; pNearest->m_LastClickTime[SDL_BUTTON_LEFT] = timer_Time(); if (timeElapsed < SELECT_DBLCLICK_RATE) ret = pNearest->SendMouseEvent(GUIM_MOUSE_DBLCLICK_LEFT, EventNameMouseLeftDoubleClick); else ret = pNearest->SendMouseEvent(GUIM_MOUSE_RELEASE_LEFT, EventNameMouseLeftRelease); } break; case SDL_BUTTON_RIGHT: if (pNearest) { double timeElapsed = timer_Time() - pNearest->m_LastClickTime[SDL_BUTTON_RIGHT]; pNearest->m_LastClickTime[SDL_BUTTON_RIGHT] = timer_Time(); if (timeElapsed < SELECT_DBLCLICK_RATE) ret = pNearest->SendMouseEvent(GUIM_MOUSE_DBLCLICK_RIGHT, EventNameMouseRightDoubleClick); else ret = pNearest->SendMouseEvent(GUIM_MOUSE_RELEASE_RIGHT, EventNameMouseRightRelease); } break; } // Reset all states on all visible objects m_BaseObject.RecurseObject(&IGUIObject::IsHidden, &IGUIObject::ResetStates); // Since the hover state will have been reset, we reload it. m_BaseObject.RecurseObject(&IGUIObject::IsHiddenOrGhost, &IGUIObject::UpdateMouseOver, static_cast(pNearest)); } } // BUTTONUP's effect on m_MouseButtons is handled after // everything else, so that e.g. 'press' handlers (activated // on button up) see which mouse button had been pressed. if (ev->ev.type == SDL_MOUSEBUTTONUP) { switch (ev->ev.button.button) { case SDL_BUTTON_LEFT: case SDL_BUTTON_RIGHT: case SDL_BUTTON_MIDDLE: m_MouseButtons &= ~Bit(ev->ev.button.button); break; default: break; } } // Restore m_MousePos (for delayed mouse button events) if (ev->ev.type == SDL_MOUSEBUTTONDOWN || ev->ev.type == SDL_MOUSEBUTTONUP) m_MousePos = oldMousePos; // Handle keys for input boxes if (GetFocusedObject()) { if ((ev->ev.type == SDL_KEYDOWN && ev->ev.key.keysym.sym != SDLK_ESCAPE && !g_keys[SDLK_LCTRL] && !g_keys[SDLK_RCTRL] && !g_keys[SDLK_LALT] && !g_keys[SDLK_RALT]) || ev->ev.type == SDL_HOTKEYDOWN || ev->ev.type == SDL_TEXTINPUT || ev->ev.type == SDL_TEXTEDITING) { ret = GetFocusedObject()->ManuallyHandleEvent(ev); } // else will return IN_PASS because we never used the button. } return ret; } void CGUI::TickObjects() { SendEventToAll(EventNameTick); m_Tooltip.Update(FindObjectUnderMouse(), m_MousePos, *this); } void CGUI::SendEventToAll(const CStr& eventName) { m_BaseObject.RecurseObject(nullptr, &IGUIObject::ScriptEvent, eventName); } void CGUI::SendEventToAll(const CStr& eventName, const JS::HandleValueArray& paramData) { m_BaseObject.RecurseObject(nullptr, &IGUIObject::ScriptEvent, eventName, paramData); } void CGUI::Draw() { // Clear the depth buffer, so the GUI is // drawn on top of everything else glClear(GL_DEPTH_BUFFER_BIT); m_BaseObject.RecurseObject(&IGUIObject::IsHidden, &IGUIObject::Draw); } void CGUI::DrawSprite(const CGUISpriteInstance& Sprite, int CellID, const float& Z, const CRect& Rect, const CRect& UNUSED(Clipping)) { // If the sprite doesn't exist (name == ""), don't bother drawing anything if (!Sprite) return; // TODO: Clipping? Sprite.Draw(*this, Rect, CellID, m_Sprites, Z); } void CGUI::UpdateResolution() { m_BaseObject.RecurseObject(nullptr, &IGUIObject::UpdateCachedSize); } IGUIObject* CGUI::ConstructObject(const CStr& str) { std::map::iterator it = m_ObjectTypes.find(str); if (it == m_ObjectTypes.end()) return nullptr; return (*it->second)(*this); } bool CGUI::AddObject(IGUIObject& parent, IGUIObject& child) { if (child.m_Name.empty()) { LOGERROR("Can't register an object without name!"); return false; } if (m_pAllObjects.find(child.m_Name) != m_pAllObjects.end()) { LOGERROR("Can't register more than one object of the name %s", child.m_Name.c_str()); return false; } m_pAllObjects[child.m_Name] = &child; parent.AddChild(child); return true; } bool CGUI::ObjectExists(const CStr& Name) const { return m_pAllObjects.find(Name) != m_pAllObjects.end(); } IGUIObject* CGUI::FindObjectByName(const CStr& Name) const { map_pObjects::const_iterator it = m_pAllObjects.find(Name); if (it == m_pAllObjects.end()) return nullptr; return it->second; } IGUIObject* CGUI::FindObjectUnderMouse() { IGUIObject* pNearest = nullptr; m_BaseObject.RecurseObject(&IGUIObject::IsHiddenOrGhost, &IGUIObject::ChooseMouseOverAndClosest, pNearest); return pNearest; } void CGUI::SetFocusedObject(IGUIObject* pObject) { if (pObject == m_FocusedObject) return; if (m_FocusedObject) { SGUIMessage msg(GUIM_LOST_FOCUS); m_FocusedObject->HandleMessage(msg); } m_FocusedObject = pObject; if (m_FocusedObject) { SGUIMessage msg(GUIM_GOT_FOCUS); m_FocusedObject->HandleMessage(msg); } } void CGUI::SetObjectHotkey(IGUIObject* pObject, const CStr& hotkeyTag) { if (!hotkeyTag.empty()) m_HotkeyObjects[hotkeyTag].push_back(pObject); } void CGUI::UnsetObjectHotkey(IGUIObject* pObject, const CStr& hotkeyTag) { if (hotkeyTag.empty()) return; std::vector& assignment = m_HotkeyObjects[hotkeyTag]; assignment.erase( std::remove_if( assignment.begin(), assignment.end(), [&pObject](const IGUIObject* hotkeyObject) { return pObject == hotkeyObject; }), assignment.end()); } void CGUI::SetGlobalHotkey(const CStr& hotkeyTag, const CStr& eventName, JS::HandleValue function) { ScriptInterface::Request rq(*m_ScriptInterface); if (hotkeyTag.empty()) { JS_ReportError(rq.cx, "Cannot assign a function to an empty hotkey identifier!"); return; } // Only support "Press", "Keydown" and "Release" events. if (eventName != EventNamePress && eventName != EventNameKeyDown && eventName != EventNameRelease) { JS_ReportError(rq.cx, "Cannot assign a function to an unsupported event!"); return; } if (!function.isObject() || !JS_ObjectIsFunction(rq.cx, &function.toObject())) { JS_ReportError(rq.cx, "Cannot assign non-function value to global hotkey '%s'", hotkeyTag.c_str()); return; } UnsetGlobalHotkey(hotkeyTag, eventName); m_GlobalHotkeys[hotkeyTag][eventName].init(rq.cx, function); } void CGUI::UnsetGlobalHotkey(const CStr& hotkeyTag, const CStr& eventName) { std::map>::iterator it = m_GlobalHotkeys.find(hotkeyTag); if (it == m_GlobalHotkeys.end()) return; m_GlobalHotkeys[hotkeyTag].erase(eventName); if (m_GlobalHotkeys.count(hotkeyTag) == 0) m_GlobalHotkeys.erase(it); } const SGUIScrollBarStyle* CGUI::GetScrollBarStyle(const CStr& style) const { std::map::const_iterator it = m_ScrollBarStyles.find(style); if (it == m_ScrollBarStyles.end()) return nullptr; return &it->second; } /** * @callgraph */ void CGUI::LoadXmlFile(const VfsPath& Filename, std::unordered_set& Paths) { Paths.insert(Filename); CXeromyces XeroFile; if (XeroFile.Load(g_VFS, Filename, "gui") != PSRETURN_OK) return; XMBElement node = XeroFile.GetRoot(); CStr root_name(XeroFile.GetElementString(node.GetNodeName())); if (root_name == "objects") Xeromyces_ReadRootObjects(node, &XeroFile, Paths); else if (root_name == "sprites") Xeromyces_ReadRootSprites(node, &XeroFile); else if (root_name == "styles") Xeromyces_ReadRootStyles(node, &XeroFile); else if (root_name == "setup") Xeromyces_ReadRootSetup(node, &XeroFile); else LOGERROR("CGUI::LoadXmlFile encountered an unknown XML root node type: %s", root_name.c_str()); } void CGUI::LoadedXmlFiles() { m_BaseObject.RecurseObject(nullptr, &IGUIObject::UpdateCachedSize); SGUIMessage msg(GUIM_LOAD); m_BaseObject.RecurseObject(nullptr, &IGUIObject::HandleMessage, msg); SendEventToAll(EventNameLoad); } //=================================================================== // XML Reading Xeromyces Specific Sub-Routines //=================================================================== void CGUI::Xeromyces_ReadRootObjects(XMBElement Element, CXeromyces* pFile, std::unordered_set& Paths) { int el_script = pFile->GetElementID("script"); std::vector > subst; // Iterate main children // they should all be or