Index: ps/trunk/source/gui/GUIManager.cpp =================================================================== --- ps/trunk/source/gui/GUIManager.cpp (revision 27944) +++ ps/trunk/source/gui/GUIManager.cpp (revision 27945) @@ -1,449 +1,449 @@ /* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "GUIManager.h" #include "gui/CGUI.h" #include "lib/timer.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/GameSetup/Config.h" #include "ps/Profile.h" #include "ps/VideoMode.h" #include "ps/XML/Xeromyces.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/Object.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/StructuredClone.h" namespace { const CStr EVENT_NAME_GAME_LOAD_PROGRESS = "GameLoadProgress"; const CStr EVENT_NAME_WINDOW_RESIZED = "WindowResized"; } // anonymous namespace CGUIManager* g_GUI = nullptr; // General TODOs: // // A lot of the CGUI data could (and should) be shared between // multiple pages, instead of treating them as completely independent, to save // memory and loading time. // called from main loop when (input) events are received. // event is passed to other handlers if false is returned. // trampoline: we don't want to make the HandleEvent implementation static InReaction gui_handler(const SDL_Event_* ev) { if (!g_GUI) return IN_PASS; PROFILE("GUI event handler"); return g_GUI->HandleEvent(ev); } static Status ReloadChangedFileCB(void* param, const VfsPath& path) { return static_cast(param)->ReloadChangedFile(path); } CGUIManager::CGUIManager() { m_ScriptContext = g_ScriptContext; m_ScriptInterface.reset(new ScriptInterface("Engine", "GUIManager", m_ScriptContext)); m_ScriptInterface->SetCallbackData(this); m_ScriptInterface->LoadGlobalScripts(); if (!CXeromyces::AddValidator(g_VFS, "gui_page", "gui/gui_page.rng")) LOGERROR("CGUIManager: failed to load GUI page grammar file 'gui/gui_page.rng'"); if (!CXeromyces::AddValidator(g_VFS, "gui", "gui/gui.rng")) LOGERROR("CGUIManager: failed to load GUI XML grammar file 'gui/gui.rng'"); RegisterFileReloadFunc(ReloadChangedFileCB, this); } CGUIManager::~CGUIManager() { UnregisterFileReloadFunc(ReloadChangedFileCB, this); } size_t CGUIManager::GetPageCount() const { return m_PageStack.size(); } void CGUIManager::SwitchPage(const CStrW& pageName, const ScriptInterface* srcScriptInterface, JS::HandleValue initData) { // The page stack is cleared (including the script context where initData came from), // therefore we have to clone initData. Script::StructuredClone initDataClone; if (!initData.isUndefined()) { ScriptRequest rq(srcScriptInterface); initDataClone = Script::WriteStructuredClone(rq, initData); } if (!m_PageStack.empty()) { // Make sure we unfocus anything on the current page. m_PageStack.back().gui->SendFocusMessage(GUIM_LOST_FOCUS); m_PageStack.clear(); } PushPage(pageName, initDataClone, JS::UndefinedHandleValue); } void CGUIManager::PushPage(const CStrW& pageName, Script::StructuredClone initData, JS::HandleValue callbackFunction) { // Store the callback handler in the current GUI page before opening the new one if (!m_PageStack.empty() && !callbackFunction.isUndefined()) { m_PageStack.back().SetCallbackFunction(*m_ScriptInterface, callbackFunction); // Make sure we unfocus anything on the current page. m_PageStack.back().gui->SendFocusMessage(GUIM_LOST_FOCUS); } // Push the page prior to loading its contents, because that may push // another GUI page on init which should be pushed on top of this new page. m_PageStack.emplace_back(pageName, initData); m_PageStack.back().LoadPage(m_ScriptContext); } void CGUIManager::PopPage(Script::StructuredClone args) { if (m_PageStack.size() < 2) { debug_warn(L"Tried to pop GUI page when there's < 2 in the stack"); return; } // Make sure we unfocus anything on the current page. m_PageStack.back().gui->SendFocusMessage(GUIM_LOST_FOCUS); m_PageStack.pop_back(); m_PageStack.back().PerformCallbackFunction(args); // We return to a page where some object might have been focused. m_PageStack.back().gui->SendFocusMessage(GUIM_GOT_FOCUS); } CGUIManager::SGUIPage::SGUIPage(const CStrW& pageName, const Script::StructuredClone initData) : m_Name(pageName), initData(initData), inputs(), gui(), callbackFunction() { } void CGUIManager::SGUIPage::LoadPage(std::shared_ptr scriptContext) { // If we're hotloading then try to grab some data from the previous page Script::StructuredClone hotloadData; if (gui) { std::shared_ptr scriptInterface = gui->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue global(rq.cx, rq.globalValue()); JS::RootedValue hotloadDataVal(rq.cx); ScriptFunction::Call(rq, global, "getHotloadData", &hotloadDataVal); hotloadData = Script::WriteStructuredClone(rq, hotloadDataVal); } g_VideoMode.ResetCursor(); inputs.clear(); gui.reset(new CGUI(scriptContext)); gui->AddObjectTypes(); VfsPath path = VfsPath("gui") / m_Name; inputs.insert(path); CXeromyces xero; if (xero.Load(g_VFS, path, "gui_page") != PSRETURN_OK) // Fail silently (Xeromyces reported the error) return; int elmt_page = xero.GetElementID("page"); int elmt_include = xero.GetElementID("include"); XMBElement root = xero.GetRoot(); if (root.GetNodeName() != elmt_page) { LOGERROR("GUI page '%s' must have root element ", utf8_from_wstring(m_Name)); return; } XERO_ITER_EL(root, node) { if (node.GetNodeName() != elmt_include) { LOGERROR("GUI page '%s' must only have elements inside ", utf8_from_wstring(m_Name)); continue; } CStr8 name = node.GetText(); CStrW nameW = node.GetText().FromUTF8(); PROFILE2("load gui xml"); PROFILE2_ATTR("name: %s", name.c_str()); TIMER(nameW.c_str()); if (name.back() == '/') { VfsPath currentDirectory = VfsPath("gui") / nameW; VfsPaths directories; vfs::GetPathnames(g_VFS, currentDirectory, L"*.xml", directories); for (const VfsPath& directory : directories) gui->LoadXmlFile(directory, inputs); } else { VfsPath directory = VfsPath("gui") / nameW; gui->LoadXmlFile(directory, inputs); } } gui->LoadedXmlFiles(); std::shared_ptr scriptInterface = gui->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue initDataVal(rq.cx); JS::RootedValue hotloadDataVal(rq.cx); JS::RootedValue global(rq.cx, rq.globalValue()); if (initData) Script::ReadStructuredClone(rq, initData, &initDataVal); if (hotloadData) Script::ReadStructuredClone(rq, hotloadData, &hotloadDataVal); if (Script::HasProperty(rq, global, "init") && !ScriptFunction::CallVoid(rq, global, "init", initDataVal, hotloadDataVal)) LOGERROR("GUI page '%s': Failed to call init() function", utf8_from_wstring(m_Name)); } void CGUIManager::SGUIPage::SetCallbackFunction(ScriptInterface& scriptInterface, JS::HandleValue callbackFunc) { if (!callbackFunc.isObject()) { LOGERROR("Given callback handler is not an object!"); return; } ScriptRequest rq(scriptInterface); if (!JS_ObjectIsFunction(&callbackFunc.toObject())) { LOGERROR("Given callback handler is not a function!"); return; } callbackFunction = std::make_shared(scriptInterface.GetGeneralJSContext(), callbackFunc); } void CGUIManager::SGUIPage::PerformCallbackFunction(Script::StructuredClone args) { if (!callbackFunction) return; std::shared_ptr scriptInterface = gui->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedObject globalObj(rq.cx, rq.glob); JS::RootedValue funcVal(rq.cx, *callbackFunction); // Delete the callback function, so that it is not called again callbackFunction.reset(); JS::RootedValue argVal(rq.cx); if (args) Script::ReadStructuredClone(rq, args, &argVal); JS::RootedValueVector paramData(rq.cx); ignore_result(paramData.append(argVal)); JS::RootedValue result(rq.cx); if(!JS_CallFunctionValue(rq.cx, globalObj, funcVal, paramData, &result)) ScriptException::CatchPending(rq); } Status CGUIManager::ReloadChangedFile(const VfsPath& path) { for (SGUIPage& p : m_PageStack) if (p.inputs.find(path) != p.inputs.end()) { LOGMESSAGE("GUI file '%s' changed - reloading page '%s'", path.string8(), utf8_from_wstring(p.m_Name)); p.LoadPage(m_ScriptContext); // TODO: this can crash if LoadPage runs an init script which modifies the page stack and breaks our iterators } return INFO::OK; } Status CGUIManager::ReloadAllPages() { // TODO: this can crash if LoadPage runs an init script which modifies the page stack and breaks our iterators for (SGUIPage& p : m_PageStack) p.LoadPage(m_ScriptContext); return INFO::OK; } InReaction CGUIManager::HandleEvent(const SDL_Event_* ev) { // We want scripts to have access to the raw input events, so they can do complex // processing when necessary (e.g. for unit selection and camera movement). // Sometimes they'll want to be layered behind the GUI widgets (e.g. to detect mousedowns on the // visible game area), sometimes they'll want to intercepts events before the GUI (e.g. // to capture all mouse events until a mouseup after dragging). // So we call two separate handler functions: bool handled = false; { PROFILE("handleInputBeforeGui"); ScriptRequest rq(*top()->GetScriptInterface()); JS::RootedValue global(rq.cx, rq.globalValue()); if (ScriptFunction::Call(rq, global, "handleInputBeforeGui", handled, *ev, top()->FindObjectUnderMouse())) if (handled) return IN_HANDLED; } { PROFILE("handle event in native GUI"); InReaction r = top()->HandleEvent(ev); if (r != IN_PASS) return r; } { // We can't take the following lines out of this scope because top() may be another gui page than it was when calling handleInputBeforeGui! ScriptRequest rq(*top()->GetScriptInterface()); JS::RootedValue global(rq.cx, rq.globalValue()); PROFILE("handleInputAfterGui"); if (ScriptFunction::Call(rq, global, "handleInputAfterGui", handled, *ev)) if (handled) return IN_HANDLED; } return IN_PASS; } void CGUIManager::SendEventToAll(const CStr& eventName) const { // Save an immutable copy so iterators aren't invalidated by handlers PageStackType pageStack = m_PageStack; for (const SGUIPage& p : pageStack) p.gui->SendEventToAll(eventName); } void CGUIManager::SendEventToAll(const CStr& eventName, JS::HandleValueArray paramData) const { // Save an immutable copy so iterators aren't invalidated by handlers PageStackType pageStack = m_PageStack; for (const SGUIPage& p : pageStack) p.gui->SendEventToAll(eventName, paramData); } void CGUIManager::TickObjects() { PROFILE3("gui tick"); // We share the script context with everything else that runs in the same thread. // This call makes sure we trigger GC regularly even if the simulation is not running. - m_ScriptInterface->GetContext()->MaybeIncrementalGC(1.0f); + m_ScriptInterface->GetContext().MaybeIncrementalGC(1.0f); // Save an immutable copy so iterators aren't invalidated by tick handlers PageStackType pageStack = m_PageStack; for (const SGUIPage& p : pageStack) p.gui->TickObjects(); } void CGUIManager::Draw(CCanvas2D& canvas) const { PROFILE3_GPU("gui"); for (const SGUIPage& p : m_PageStack) p.gui->Draw(canvas); } void CGUIManager::UpdateResolution() { // Save an immutable copy so iterators aren't invalidated by event handlers PageStackType pageStack = m_PageStack; for (const SGUIPage& p : pageStack) { p.gui->UpdateResolution(); p.gui->SendEventToAll(EVENT_NAME_WINDOW_RESIZED); } } bool CGUIManager::TemplateExists(const std::string& templateName) const { return m_TemplateLoader.TemplateExists(templateName); } const CParamNode& CGUIManager::GetTemplate(const std::string& templateName) { const CParamNode& templateRoot = m_TemplateLoader.GetTemplateFileData(templateName).GetOnlyChild(); if (!templateRoot.IsOk()) LOGERROR("Invalid template found for '%s'", templateName.c_str()); return templateRoot; } void CGUIManager::DisplayLoadProgress(int percent, const wchar_t* pending_task) { const ScriptInterface& scriptInterface = *(GetActiveGUI()->GetScriptInterface()); ScriptRequest rq(scriptInterface); JS::RootedValueVector paramData(rq.cx); ignore_result(paramData.append(JS::NumberValue(percent))); JS::RootedValue valPendingTask(rq.cx); Script::ToJSVal(rq, &valPendingTask, pending_task); ignore_result(paramData.append(valPendingTask)); SendEventToAll(EVENT_NAME_GAME_LOAD_PROGRESS, paramData); } // This returns a shared_ptr to make sure the CGUI doesn't get deallocated // while we're in the middle of calling a function on it (e.g. if a GUI script // calls SwitchPage) std::shared_ptr CGUIManager::top() const { ENSURE(m_PageStack.size()); return m_PageStack.back().gui; } Index: ps/trunk/source/network/NetServer.cpp =================================================================== --- ps/trunk/source/network/NetServer.cpp (revision 27944) +++ ps/trunk/source/network/NetServer.cpp (revision 27945) @@ -1,1749 +1,1749 @@ /* Copyright (C) 2023 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 "NetServer.h" #include "NetClient.h" #include "NetEnet.h" #include "NetMessage.h" #include "NetSession.h" #include "NetServerTurnManager.h" #include "NetStats.h" #include "lib/external_libraries/enet.h" #include "lib/types.h" #include "network/StunClient.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/GUID.h" #include "ps/Hashing.h" #include "ps/Profile.h" #include "ps/Threading.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/JSON.h" #include "simulation2/Simulation2.h" #include "simulation2/system/TurnManager.h" #if CONFIG2_MINIUPNPC #include #include #include #include #endif #include /** * Number of peers to allocate for the enet host. * Limited by ENET_PROTOCOL_MAXIMUM_PEER_ID (4096). * * At most 8 players, 32 observers and 1 temporary connection to send the "server full" disconnect-reason. */ #define MAX_CLIENTS 41 #define DEFAULT_SERVER_NAME L"Unnamed Server" constexpr int CHANNEL_COUNT = 1; constexpr int FAILED_PASSWORD_TRIES_BEFORE_BAN = 3; /** * enet_host_service timeout (msecs). * Smaller numbers may hurt performance; larger numbers will * hurt latency responding to messages from game thread. */ static const int HOST_SERVICE_TIMEOUT = 50; /** * Once ping goes above turn length * command delay, * the game will start 'freezing' for other clients while we catch up. * Since commands are sent client -> server -> client, divide by 2. * (duplicated in NetServer.cpp to avoid having to fetch the constants in a header file) */ constexpr u32 NETWORK_BAD_PING = DEFAULT_TURN_LENGTH * COMMAND_DELAY_MP / 2; CNetServer* g_NetServer = NULL; static CStr DebugName(CNetServerSession* session) { if (session == NULL) return "[unknown host]"; if (session->GetGUID().empty()) return "[unauthed host]"; return "[" + session->GetGUID().substr(0, 8) + "...]"; } /** * Async task for receiving the initial game state to be forwarded to another * client that is rejoining an in-progress network game. */ class CNetFileReceiveTask_ServerRejoin : public CNetFileReceiveTask { NONCOPYABLE(CNetFileReceiveTask_ServerRejoin); public: CNetFileReceiveTask_ServerRejoin(CNetServerWorker& server, u32 hostID) : m_Server(server), m_RejoinerHostID(hostID) { } virtual void OnComplete() { // We've received the game state from an existing player - now // we need to send it onwards to the newly rejoining player // Find the session corresponding to the rejoining host (if any) CNetServerSession* session = NULL; for (CNetServerSession* serverSession : m_Server.m_Sessions) { if (serverSession->GetHostID() == m_RejoinerHostID) { session = serverSession; break; } } if (!session) { LOGMESSAGE("Net server: rejoining client disconnected before we sent to it"); return; } // Store the received state file, and tell the client to start downloading it from us // TODO: this will get kind of confused if there's multiple clients downloading in parallel; // they'll race and get whichever happens to be the latest received by the server, // which should still work but isn't great m_Server.m_JoinSyncFile = m_Buffer; // Send the init attributes alongside - these should be correct since the game should be started. CJoinSyncStartMessage message; message.m_InitAttributes = Script::StringifyJSON(ScriptRequest(m_Server.GetScriptInterface()), &m_Server.m_InitAttributes); session->SendMessage(&message); } private: CNetServerWorker& m_Server; u32 m_RejoinerHostID; }; /* * XXX: We use some non-threadsafe functions from the worker thread. * See http://trac.wildfiregames.com/ticket/654 */ CNetServerWorker::CNetServerWorker(bool useLobbyAuth) : m_LobbyAuth(useLobbyAuth), m_Shutdown(false), m_ScriptInterface(NULL), m_NextHostID(1), m_Host(NULL), m_ControllerGUID(), m_Stats(NULL), m_LastConnectionCheck(0) { m_State = SERVER_STATE_UNCONNECTED; m_ServerTurnManager = NULL; m_ServerName = DEFAULT_SERVER_NAME; } CNetServerWorker::~CNetServerWorker() { if (m_State != SERVER_STATE_UNCONNECTED) { // Tell the thread to shut down { std::lock_guard lock(m_WorkerMutex); m_Shutdown = true; } // Wait for it to shut down cleanly m_WorkerThread.join(); } #if CONFIG2_MINIUPNPC if (m_UPnPThread.joinable()) m_UPnPThread.detach(); #endif // Clean up resources delete m_Stats; for (CNetServerSession* session : m_Sessions) { session->DisconnectNow(NDR_SERVER_SHUTDOWN); delete session; } if (m_Host) enet_host_destroy(m_Host); delete m_ServerTurnManager; } void CNetServerWorker::SetPassword(const CStr& hashedPassword) { m_Password = hashedPassword; } void CNetServerWorker::SetControllerSecret(const std::string& secret) { m_ControllerSecret = secret; } bool CNetServerWorker::CheckPassword(const std::string& password, const std::string& salt) const { return HashCryptographically(m_Password, salt) == password; } bool CNetServerWorker::SetupConnection(const u16 port) { ENSURE(m_State == SERVER_STATE_UNCONNECTED); ENSURE(!m_Host); // Bind to default host ENetAddress addr; addr.host = ENET_HOST_ANY; addr.port = port; // Create ENet server m_Host = PS::Enet::CreateHost(&addr, MAX_CLIENTS, CHANNEL_COUNT); if (!m_Host) { LOGERROR("Net server: enet_host_create failed"); return false; } m_Stats = new CNetStatsTable(); if (CProfileViewer::IsInitialised()) g_ProfileViewer.AddRootTable(m_Stats); m_State = SERVER_STATE_PREGAME; // Launch the worker thread m_WorkerThread = std::thread(Threading::HandleExceptions::Wrapper, this); #if CONFIG2_MINIUPNPC // Launch the UPnP thread m_UPnPThread = std::thread(Threading::HandleExceptions::Wrapper); #endif return true; } #if CONFIG2_MINIUPNPC void CNetServerWorker::SetupUPnP() { debug_SetThreadName("UPnP"); // Values we want to set. char psPort[6]; sprintf_s(psPort, ARRAY_SIZE(psPort), "%d", PS_DEFAULT_PORT); const char* leaseDuration = "0"; // Indefinite/permanent lease duration. const char* description = "0AD Multiplayer"; const char* protocall = "UDP"; char internalIPAddress[64]; char externalIPAddress[40]; // Variables to hold the values that actually get set. char intClient[40]; char intPort[6]; char duration[16]; // Intermediate variables. bool allocatedUrls = false; struct UPNPUrls urls; struct IGDdatas data; struct UPNPDev* devlist = NULL; // Make sure everything is properly freed. std::function freeUPnP = [&allocatedUrls, &urls, &devlist]() { if (allocatedUrls) FreeUPNPUrls(&urls); freeUPNPDevlist(devlist); // IGDdatas does not need to be freed according to UPNP_GetIGDFromUrl }; // Cached root descriptor URL. std::string rootDescURL; CFG_GET_VAL("network.upnprootdescurl", rootDescURL); if (!rootDescURL.empty()) LOGMESSAGE("Net server: attempting to use cached root descriptor URL: %s", rootDescURL.c_str()); int ret = 0; // Try a cached URL first if (!rootDescURL.empty() && UPNP_GetIGDFromUrl(rootDescURL.c_str(), &urls, &data, internalIPAddress, sizeof(internalIPAddress))) { LOGMESSAGE("Net server: using cached IGD = %s", urls.controlURL); ret = 1; } // No cached URL, or it did not respond. Try getting a valid UPnP device for 10 seconds. #if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 14 else if ((devlist = upnpDiscover(10000, 0, 0, 0, 0, 2, 0)) != NULL) #else else if ((devlist = upnpDiscover(10000, 0, 0, 0, 0, 0)) != NULL) #endif { ret = UPNP_GetValidIGD(devlist, &urls, &data, internalIPAddress, sizeof(internalIPAddress)); allocatedUrls = ret != 0; // urls is allocated on non-zero return values } else { LOGMESSAGE("Net server: upnpDiscover failed and no working cached URL."); freeUPnP(); return; } switch (ret) { case 0: LOGMESSAGE("Net server: No IGD found"); break; case 1: LOGMESSAGE("Net server: found valid IGD = %s", urls.controlURL); break; case 2: LOGMESSAGE("Net server: found a valid, not connected IGD = %s, will try to continue anyway", urls.controlURL); break; case 3: LOGMESSAGE("Net server: found a UPnP device unrecognized as IGD = %s, will try to continue anyway", urls.controlURL); break; default: debug_warn(L"Unrecognized return value from UPNP_GetValidIGD"); } // Try getting our external/internet facing IP. TODO: Display this on the game-setup page for conviniance. ret = UPNP_GetExternalIPAddress(urls.controlURL, data.first.servicetype, externalIPAddress); if (ret != UPNPCOMMAND_SUCCESS) { LOGMESSAGE("Net server: GetExternalIPAddress failed with code %d (%s)", ret, strupnperror(ret)); freeUPnP(); return; } LOGMESSAGE("Net server: ExternalIPAddress = %s", externalIPAddress); // Try to setup port forwarding. ret = UPNP_AddPortMapping(urls.controlURL, data.first.servicetype, psPort, psPort, internalIPAddress, description, protocall, 0, leaseDuration); if (ret != UPNPCOMMAND_SUCCESS) { LOGMESSAGE("Net server: AddPortMapping(%s, %s, %s) failed with code %d (%s)", psPort, psPort, internalIPAddress, ret, strupnperror(ret)); freeUPnP(); return; } // Check that the port was actually forwarded. ret = UPNP_GetSpecificPortMappingEntry(urls.controlURL, data.first.servicetype, psPort, protocall, #if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 10 NULL/*remoteHost*/, #endif intClient, intPort, NULL/*desc*/, NULL/*enabled*/, duration); if (ret != UPNPCOMMAND_SUCCESS) { LOGMESSAGE("Net server: GetSpecificPortMappingEntry() failed with code %d (%s)", ret, strupnperror(ret)); freeUPnP(); return; } LOGMESSAGE("Net server: External %s:%s %s is redirected to internal %s:%s (duration=%s)", externalIPAddress, psPort, protocall, intClient, intPort, duration); // Cache root descriptor URL to try to avoid discovery next time. g_ConfigDB.SetValueString(CFG_USER, "network.upnprootdescurl", urls.controlURL); g_ConfigDB.WriteValueToFile(CFG_USER, "network.upnprootdescurl", urls.controlURL); LOGMESSAGE("Net server: cached UPnP root descriptor URL as %s", urls.controlURL); freeUPnP(); } #endif // CONFIG2_MINIUPNPC bool CNetServerWorker::SendMessage(ENetPeer* peer, const CNetMessage* message) { ENSURE(m_Host); CNetServerSession* session = static_cast(peer->data); return CNetHost::SendMessage(message, peer, DebugName(session).c_str()); } bool CNetServerWorker::Broadcast(const CNetMessage* message, const std::vector& targetStates) { ENSURE(m_Host); bool ok = true; // TODO: this does lots of repeated message serialisation if we have lots // of remote peers; could do it more efficiently if that's a real problem for (CNetServerSession* session : m_Sessions) if (std::find(targetStates.begin(), targetStates.end(), static_cast(session->GetCurrState())) != targetStates.end() && !session->SendMessage(message)) ok = false; return ok; } void CNetServerWorker::RunThread(CNetServerWorker* data) { debug_SetThreadName("NetServer"); data->Run(); } void CNetServerWorker::Run() { // The script context uses the profiler and therefore the thread must be registered before the context is created g_Profiler2.RegisterCurrentThread("Net server"); // We create a new ScriptContext for this network thread, with a single ScriptInterface. std::shared_ptr netServerContext = ScriptContext::CreateContext(); m_ScriptInterface = new ScriptInterface("Engine", "Net server", netServerContext); m_InitAttributes.init(m_ScriptInterface->GetGeneralJSContext(), JS::UndefinedValue()); while (true) { if (!RunStep()) break; // Update profiler stats m_Stats->LatchHostState(m_Host); } // Clear roots before deleting their context m_SavedCommands.clear(); SAFE_DELETE(m_ScriptInterface); } bool CNetServerWorker::RunStep() { // Check for messages from the game thread. // (Do as little work as possible while the mutex is held open, // to avoid performance problems and deadlocks.) - m_ScriptInterface->GetContext()->MaybeIncrementalGC(0.5f); + m_ScriptInterface->GetContext().MaybeIncrementalGC(0.5f); ScriptRequest rq(m_ScriptInterface); std::vector newStartGame; std::vector newGameAttributes; std::vector> newLobbyAuths; std::vector newTurnLength; { std::lock_guard lock(m_WorkerMutex); if (m_Shutdown) return false; newStartGame.swap(m_StartGameQueue); newGameAttributes.swap(m_InitAttributesQueue); newLobbyAuths.swap(m_LobbyAuthQueue); newTurnLength.swap(m_TurnLengthQueue); } if (!newGameAttributes.empty()) { if (m_State != SERVER_STATE_UNCONNECTED && m_State != SERVER_STATE_PREGAME) LOGERROR("NetServer: Init Attributes cannot be changed after the server starts loading."); else { JS::RootedValue gameAttributesVal(rq.cx); Script::ParseJSON(rq, newGameAttributes.back(), &gameAttributesVal); m_InitAttributes = gameAttributesVal; } } if (!newTurnLength.empty()) SetTurnLength(newTurnLength.back()); while (!newLobbyAuths.empty()) { const std::pair& auth = newLobbyAuths.back(); ProcessLobbyAuth(auth.first, auth.second); newLobbyAuths.pop_back(); } // Perform file transfers for (CNetServerSession* session : m_Sessions) session->GetFileTransferer().Poll(); CheckClientConnections(); // Process network events: ENetEvent event; int status = enet_host_service(m_Host, &event, HOST_SERVICE_TIMEOUT); if (status < 0) { LOGERROR("CNetServerWorker: enet_host_service failed (%d)", status); // TODO: notify game that the server has shut down return false; } if (status == 0) { // Reached timeout with no events - try again return true; } // Process the event: switch (event.type) { case ENET_EVENT_TYPE_CONNECT: { // Report the client address char hostname[256] = "(error)"; enet_address_get_host_ip(&event.peer->address, hostname, ARRAY_SIZE(hostname)); LOGMESSAGE("Net server: Received connection from %s:%u", hostname, (unsigned int)event.peer->address.port); // Set up a session object for this peer CNetServerSession* session = new CNetServerSession(*this, event.peer); m_Sessions.push_back(session); SetupSession(session); ENSURE(event.peer->data == NULL); event.peer->data = session; HandleConnect(session); break; } case ENET_EVENT_TYPE_DISCONNECT: { // If there is an active session with this peer, then reset and delete it CNetServerSession* session = static_cast(event.peer->data); if (session) { LOGMESSAGE("Net server: Disconnected %s", DebugName(session).c_str()); // Remove the session first, so we won't send player-update messages to it // when updating the FSM m_Sessions.erase(remove(m_Sessions.begin(), m_Sessions.end(), session), m_Sessions.end()); session->Update((uint)NMT_CONNECTION_LOST, NULL); delete session; event.peer->data = NULL; } if (m_State == SERVER_STATE_LOADING) CheckGameLoadStatus(NULL); break; } case ENET_EVENT_TYPE_RECEIVE: { // If there is an active session with this peer, then process the message CNetServerSession* session = static_cast(event.peer->data); if (session) { // Create message from raw data CNetMessage* msg = CNetMessageFactory::CreateMessage(event.packet->data, event.packet->dataLength, GetScriptInterface()); if (msg) { LOGMESSAGE("Net server: Received message %s of size %lu from %s", msg->ToString().c_str(), (unsigned long)msg->GetSerializedLength(), DebugName(session).c_str()); HandleMessageReceive(msg, session); delete msg; } } // Done using the packet enet_packet_destroy(event.packet); break; } case ENET_EVENT_TYPE_NONE: break; } return true; } void CNetServerWorker::CheckClientConnections() { // Send messages at most once per second std::time_t now = std::time(nullptr); if (now <= m_LastConnectionCheck) return; m_LastConnectionCheck = now; for (size_t i = 0; i < m_Sessions.size(); ++i) { u32 lastReceived = m_Sessions[i]->GetLastReceivedTime(); u32 meanRTT = m_Sessions[i]->GetMeanRTT(); CNetMessage* message = nullptr; // Report if we didn't hear from the client since few seconds if (lastReceived > NETWORK_WARNING_TIMEOUT) { CClientTimeoutMessage* msg = new CClientTimeoutMessage(); msg->m_GUID = m_Sessions[i]->GetGUID(); msg->m_LastReceivedTime = lastReceived; message = msg; } // Report if the client has bad ping else if (meanRTT > NETWORK_BAD_PING) { CClientPerformanceMessage* msg = new CClientPerformanceMessage(); CClientPerformanceMessage::S_m_Clients client; client.m_GUID = m_Sessions[i]->GetGUID(); client.m_MeanRTT = meanRTT; msg->m_Clients.push_back(client); message = msg; } // Send to all clients except the affected one // (since that will show the locally triggered warning instead). // Also send it to clients that finished the loading screen while // the game is still waiting for other clients to finish the loading screen. if (message) for (size_t j = 0; j < m_Sessions.size(); ++j) { if (i != j && ( (m_Sessions[j]->GetCurrState() == NSS_PREGAME && m_State == SERVER_STATE_PREGAME) || m_Sessions[j]->GetCurrState() == NSS_INGAME)) { m_Sessions[j]->SendMessage(message); } } SAFE_DELETE(message); } } void CNetServerWorker::HandleMessageReceive(const CNetMessage* message, CNetServerSession* session) { // Handle non-FSM messages first Status status = session->GetFileTransferer().HandleMessageReceive(*message); if (status != INFO::SKIPPED) return; if (message->GetType() == NMT_FILE_TRANSFER_REQUEST) { CFileTransferRequestMessage* reqMessage = (CFileTransferRequestMessage*)message; // Rejoining client got our JoinSyncStart after we received the state from // another client, and has now requested that we forward it to them ENSURE(!m_JoinSyncFile.empty()); session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, m_JoinSyncFile); return; } // Update FSM if (!session->Update(message->GetType(), (void*)message)) LOGERROR("Net server: Error running FSM update (type=%d state=%d)", (int)message->GetType(), (int)session->GetCurrState()); } void CNetServerWorker::SetupSession(CNetServerSession* session) { void* context = session; // Set up transitions for session session->AddTransition(NSS_UNCONNECTED, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED); session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED); session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CLIENT_HANDSHAKE, NSS_AUTHENTICATE, &OnClientHandshake, context); session->AddTransition(NSS_LOBBY_AUTHENTICATE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED); session->AddTransition(NSS_LOBBY_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, &OnAuthenticate, context); session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED); session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, &OnAuthenticate, context); session->AddTransition(NSS_PREGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, &OnDisconnect, context); session->AddTransition(NSS_PREGAME, (uint)NMT_CHAT, NSS_PREGAME, &OnChat, context); session->AddTransition(NSS_PREGAME, (uint)NMT_READY, NSS_PREGAME, &OnReady, context); session->AddTransition(NSS_PREGAME, (uint)NMT_CLEAR_ALL_READY, NSS_PREGAME, &OnClearAllReady, context); session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_SETUP, NSS_PREGAME, &OnGameSetup, context); session->AddTransition(NSS_PREGAME, (uint)NMT_ASSIGN_PLAYER, NSS_PREGAME, &OnAssignPlayer, context); session->AddTransition(NSS_PREGAME, (uint)NMT_KICKED, NSS_PREGAME, &OnKickPlayer, context); session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_START, NSS_PREGAME, &OnGameStart, context); session->AddTransition(NSS_PREGAME, (uint)NMT_LOADED_GAME, NSS_INGAME, &OnLoadedGame, context); session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_KICKED, NSS_JOIN_SYNCING, &OnKickPlayer, context); session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, &OnDisconnect, context); session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NSS_INGAME, &OnJoinSyncingLoadedGame, context); session->AddTransition(NSS_INGAME, (uint)NMT_REJOINED, NSS_INGAME, &OnRejoined, context); session->AddTransition(NSS_INGAME, (uint)NMT_KICKED, NSS_INGAME, &OnKickPlayer, context); session->AddTransition(NSS_INGAME, (uint)NMT_CLIENT_PAUSED, NSS_INGAME, &OnClientPaused, context); session->AddTransition(NSS_INGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, &OnDisconnect, context); session->AddTransition(NSS_INGAME, (uint)NMT_CHAT, NSS_INGAME, &OnChat, context); session->AddTransition(NSS_INGAME, (uint)NMT_SIMULATION_COMMAND, NSS_INGAME, &OnSimulationCommand, context); session->AddTransition(NSS_INGAME, (uint)NMT_SYNC_CHECK, NSS_INGAME, &OnSyncCheck, context); session->AddTransition(NSS_INGAME, (uint)NMT_END_COMMAND_BATCH, NSS_INGAME, &OnEndCommandBatch, context); // Set first state session->SetFirstState(NSS_HANDSHAKE); } bool CNetServerWorker::HandleConnect(CNetServerSession* session) { if (std::find(m_BannedIPs.begin(), m_BannedIPs.end(), session->GetIPAddress()) != m_BannedIPs.end()) { session->Disconnect(NDR_BANNED); return false; } CSrvHandshakeMessage handshake; handshake.m_Magic = PS_PROTOCOL_MAGIC; handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION; handshake.m_SoftwareVersion = PS_PROTOCOL_VERSION; return session->SendMessage(&handshake); } void CNetServerWorker::OnUserJoin(CNetServerSession* session) { AddPlayer(session->GetGUID(), session->GetUserName()); CPlayerAssignmentMessage assignMessage; ConstructPlayerAssignmentMessage(assignMessage); session->SendMessage(&assignMessage); } void CNetServerWorker::OnUserLeave(CNetServerSession* session) { std::vector::iterator pausing = std::find(m_PausingPlayers.begin(), m_PausingPlayers.end(), session->GetGUID()); if (pausing != m_PausingPlayers.end()) m_PausingPlayers.erase(pausing); RemovePlayer(session->GetGUID()); if (m_ServerTurnManager && session->GetCurrState() != NSS_JOIN_SYNCING) m_ServerTurnManager->UninitialiseClient(session->GetHostID()); // TODO: ought to switch the player controlled by that client // back to AI control, or something? } void CNetServerWorker::AddPlayer(const CStr& guid, const CStrW& name) { // Find all player IDs in active use; we mustn't give them to a second player (excluding the unassigned ID: -1) std::set usedIDs; for (const std::pair& p : m_PlayerAssignments) if (p.second.m_Enabled && p.second.m_PlayerID != -1) usedIDs.insert(p.second.m_PlayerID); // If the player is rejoining after disconnecting, try to give them // back their old player ID. Don't do this in pregame however, // as that ID might be invalid for various reasons. i32 playerID = -1; if (m_State != SERVER_STATE_UNCONNECTED && m_State != SERVER_STATE_PREGAME) { // Try to match GUID first for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it) { if (!it->second.m_Enabled && it->first == guid && usedIDs.find(it->second.m_PlayerID) == usedIDs.end()) { playerID = it->second.m_PlayerID; m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now goto found; } } // Try to match username next for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it) { if (!it->second.m_Enabled && it->second.m_Name == name && usedIDs.find(it->second.m_PlayerID) == usedIDs.end()) { playerID = it->second.m_PlayerID; m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now goto found; } } } found: PlayerAssignment assignment; assignment.m_Enabled = true; assignment.m_Name = name; assignment.m_PlayerID = playerID; assignment.m_Status = 0; m_PlayerAssignments[guid] = assignment; // Send the new assignments to all currently active players // (which does not include the one that's just joining) SendPlayerAssignments(); } void CNetServerWorker::RemovePlayer(const CStr& guid) { m_PlayerAssignments[guid].m_Enabled = false; SendPlayerAssignments(); } void CNetServerWorker::ClearAllPlayerReady() { for (std::pair& p : m_PlayerAssignments) if (p.second.m_Status != 2) p.second.m_Status = 0; SendPlayerAssignments(); } void CNetServerWorker::KickPlayer(const CStrW& playerName, const bool ban) { // Find the user with that name std::vector::iterator it = std::find_if(m_Sessions.begin(), m_Sessions.end(), [&](CNetServerSession* session) { return session->GetUserName() == playerName; }); // and return if no one or the host has that name if (it == m_Sessions.end() || (*it)->GetGUID() == m_ControllerGUID) return; if (ban) { // Remember name if (std::find(m_BannedPlayers.begin(), m_BannedPlayers.end(), playerName) == m_BannedPlayers.end()) m_BannedPlayers.push_back(m_LobbyAuth ? CStrW(playerName.substr(0, playerName.find(L" ("))) : playerName); // Remember IP address u32 ipAddress = (*it)->GetIPAddress(); if (std::find(m_BannedIPs.begin(), m_BannedIPs.end(), ipAddress) == m_BannedIPs.end()) m_BannedIPs.push_back(ipAddress); } // Disconnect that user (*it)->Disconnect(ban ? NDR_BANNED : NDR_KICKED); // Send message notifying other clients CKickedMessage kickedMessage; kickedMessage.m_Name = playerName; kickedMessage.m_Ban = ban; Broadcast(&kickedMessage, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME }); } void CNetServerWorker::AssignPlayer(int playerID, const CStr& guid) { // Remove anyone who's already assigned to this player for (std::pair& p : m_PlayerAssignments) { if (p.second.m_PlayerID == playerID) p.second.m_PlayerID = -1; } // Update this host's assignment if it exists if (m_PlayerAssignments.find(guid) != m_PlayerAssignments.end()) m_PlayerAssignments[guid].m_PlayerID = playerID; SendPlayerAssignments(); } void CNetServerWorker::ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage& message) { for (const std::pair& p : m_PlayerAssignments) { if (!p.second.m_Enabled) continue; CPlayerAssignmentMessage::S_m_Hosts h; h.m_GUID = p.first; h.m_Name = p.second.m_Name; h.m_PlayerID = p.second.m_PlayerID; h.m_Status = p.second.m_Status; message.m_Hosts.push_back(h); } } void CNetServerWorker::SendPlayerAssignments() { CPlayerAssignmentMessage message; ConstructPlayerAssignmentMessage(message); Broadcast(&message, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME }); } const ScriptInterface& CNetServerWorker::GetScriptInterface() { return *m_ScriptInterface; } void CNetServerWorker::SetTurnLength(u32 msecs) { if (m_ServerTurnManager) m_ServerTurnManager->SetTurnLength(msecs); } void CNetServerWorker::ProcessLobbyAuth(const CStr& name, const CStr& token) { LOGMESSAGE("Net Server: Received lobby auth message from %s with %s", name, token); // Find the user with that guid std::vector::iterator it = std::find_if(m_Sessions.begin(), m_Sessions.end(), [&](CNetServerSession* session) { return session->GetGUID() == token; }); if (it == m_Sessions.end()) return; (*it)->SetUserName(name.FromUTF8()); // Send an empty message to request the authentication message from the client // after its identity has been confirmed via the lobby CAuthenticateMessage emptyMessage; (*it)->SendMessage(&emptyMessage); } bool CNetServerWorker::OnClientHandshake(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CLIENT_HANDSHAKE); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CCliHandshakeMessage* message = (CCliHandshakeMessage*)event->GetParamRef(); if (message->m_ProtocolVersion != PS_PROTOCOL_VERSION) { session->Disconnect(NDR_INCORRECT_PROTOCOL_VERSION); return false; } CStr guid = ps_generate_guid(); int count = 0; // Ensure unique GUID while(std::find_if( server.m_Sessions.begin(), server.m_Sessions.end(), [&guid] (const CNetServerSession* session) { return session->GetGUID() == guid; }) != server.m_Sessions.end()) { if (++count > 100) { session->Disconnect(NDR_GUID_FAILED); return true; } guid = ps_generate_guid(); } session->SetGUID(guid); CSrvHandshakeResponseMessage handshakeResponse; handshakeResponse.m_UseProtocolVersion = PS_PROTOCOL_VERSION; handshakeResponse.m_GUID = guid; handshakeResponse.m_Flags = 0; if (server.m_LobbyAuth) { handshakeResponse.m_Flags |= PS_NETWORK_FLAG_REQUIRE_LOBBYAUTH; session->SetNextState(NSS_LOBBY_AUTHENTICATE); } session->SendMessage(&handshakeResponse); return true; } bool CNetServerWorker::OnAuthenticate(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); // Prohibit joins while the game is loading if (server.m_State == SERVER_STATE_LOADING) { LOGMESSAGE("Refused connection while the game is loading"); session->Disconnect(NDR_SERVER_LOADING); return true; } CAuthenticateMessage* message = (CAuthenticateMessage*)event->GetParamRef(); CStrW username = SanitisePlayerName(message->m_Name); CStrW usernameWithoutRating(username.substr(0, username.find(L" ("))); // Compare the lowercase names as specified by https://xmpp.org/extensions/xep-0029.html#sect-idm139493404168176 // "[...] comparisons will be made in case-normalized canonical form." if (server.m_LobbyAuth && usernameWithoutRating.LowerCase() != session->GetUserName().LowerCase()) { LOGERROR("Net server: lobby auth: %s tried joining as %s", session->GetUserName().ToUTF8(), usernameWithoutRating.ToUTF8()); session->Disconnect(NDR_LOBBY_AUTH_FAILED); return true; } // Check the password before anything else. // NB: m_Name must match the client's salt, @see CNetClient::SetGamePassword if (!server.CheckPassword(message->m_Password, message->m_Name.ToUTF8())) { // Noisy logerror because players are not supposed to be able to get the IP, // so this might be someone targeting the host for some reason // (or TODO a dedicated server and we do want to log anyways) LOGERROR("Net server: user %s tried joining with the wrong password", session->GetUserName().ToUTF8()); session->Disconnect(NDR_SERVER_REFUSED); return true; } // Either deduplicate or prohibit join if name is in use bool duplicatePlayernames = false; CFG_GET_VAL("network.duplicateplayernames", duplicatePlayernames); // If lobby authentication is enabled, the clients playername has already been registered. // There also can't be any duplicated names. if (!server.m_LobbyAuth && duplicatePlayernames) username = server.DeduplicatePlayerName(username); else { std::vector::iterator it = std::find_if( server.m_Sessions.begin(), server.m_Sessions.end(), [&username] (const CNetServerSession* session) { return session->GetUserName() == username; }); if (it != server.m_Sessions.end() && (*it) != session) { session->Disconnect(NDR_PLAYERNAME_IN_USE); return true; } } // Disconnect banned usernames if (std::find(server.m_BannedPlayers.begin(), server.m_BannedPlayers.end(), server.m_LobbyAuth ? usernameWithoutRating : username) != server.m_BannedPlayers.end()) { session->Disconnect(NDR_BANNED); return true; } int maxObservers = 0; CFG_GET_VAL("network.observerlimit", maxObservers); bool isRejoining = false; bool serverFull = false; if (server.m_State == SERVER_STATE_PREGAME) { // Don't check for maxObservers in the gamesetup, as we don't know yet who will be assigned serverFull = server.m_Sessions.size() >= MAX_CLIENTS; } else { bool isObserver = true; int disconnectedPlayers = 0; int connectedPlayers = 0; // (TODO: if GUIDs were stable, we should use them instead) for (const std::pair& p : server.m_PlayerAssignments) { const PlayerAssignment& assignment = p.second; if (!assignment.m_Enabled && assignment.m_Name == username) { isObserver = assignment.m_PlayerID == -1; isRejoining = true; } if (assignment.m_PlayerID == -1) continue; if (assignment.m_Enabled) ++connectedPlayers; else ++disconnectedPlayers; } // Optionally allow everyone or only buddies to join after the game has started if (!isRejoining) { CStr observerLateJoin; CFG_GET_VAL("network.lateobservers", observerLateJoin); if (observerLateJoin == "everyone") { isRejoining = true; } else if (observerLateJoin == "buddies") { CStr buddies; CFG_GET_VAL("lobby.buddies", buddies); std::wstringstream buddiesStream(wstring_from_utf8(buddies)); CStrW buddy; while (std::getline(buddiesStream, buddy, L',')) { if (buddy == usernameWithoutRating) { isRejoining = true; break; } } } } if (!isRejoining) { LOGMESSAGE("Refused connection after game start from not-previously-known user \"%s\"", utf8_from_wstring(username)); session->Disconnect(NDR_SERVER_ALREADY_IN_GAME); return true; } // Ensure all players will be able to rejoin serverFull = isObserver && ( (int) server.m_Sessions.size() - connectedPlayers > maxObservers || (int) server.m_Sessions.size() + disconnectedPlayers >= MAX_CLIENTS); } if (serverFull) { session->Disconnect(NDR_SERVER_FULL); return true; } u32 newHostID = server.m_NextHostID++; session->SetUserName(username); session->SetHostID(newHostID); CAuthenticateResultMessage authenticateResult; authenticateResult.m_Code = isRejoining ? ARC_OK_REJOINING : ARC_OK; authenticateResult.m_HostID = newHostID; authenticateResult.m_Message = L"Logged in"; authenticateResult.m_IsController = 0; if (message->m_ControllerSecret == server.m_ControllerSecret) { if (server.m_ControllerGUID.empty()) { server.m_ControllerGUID = session->GetGUID(); authenticateResult.m_IsController = 1; } // TODO: we could probably handle having several controllers, or swapping? } session->SendMessage(&authenticateResult); server.OnUserJoin(session); if (isRejoining) { ENSURE(server.m_State != SERVER_STATE_UNCONNECTED && server.m_State != SERVER_STATE_PREGAME); // Request a copy of the current game state from an existing player, // so we can send it on to the new player // Assume session 0 is most likely the local player, so they're // the most efficient client to request a copy from CNetServerSession* sourceSession = server.m_Sessions.at(0); sourceSession->GetFileTransferer().StartTask( std::shared_ptr(new CNetFileReceiveTask_ServerRejoin(server, newHostID)) ); session->SetNextState(NSS_JOIN_SYNCING); } return true; } bool CNetServerWorker::OnSimulationCommand(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_SIMULATION_COMMAND); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CSimulationMessage* message = (CSimulationMessage*)event->GetParamRef(); // Ignore messages sent by one player on behalf of another player // unless cheating is enabled bool cheatsEnabled = false; const ScriptInterface& scriptInterface = server.GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue settings(rq.cx); Script::GetProperty(rq, server.m_InitAttributes, "settings", &settings); if (Script::HasProperty(rq, settings, "CheatsEnabled")) Script::GetProperty(rq, settings, "CheatsEnabled", cheatsEnabled); PlayerAssignmentMap::iterator it = server.m_PlayerAssignments.find(session->GetGUID()); // When cheating is disabled, fail if the player the message claims to // represent does not exist or does not match the sender's player name if (!cheatsEnabled && (it == server.m_PlayerAssignments.end() || it->second.m_PlayerID != message->m_Player)) return true; // Send it back to all clients that have finished // the loading screen (and the synchronization when rejoining) server.Broadcast(message, { NSS_INGAME }); // Save all the received commands if (server.m_SavedCommands.size() < message->m_Turn + 1) server.m_SavedCommands.resize(message->m_Turn + 1); server.m_SavedCommands[message->m_Turn].push_back(*message); // TODO: we shouldn't send the message back to the client that first sent it return true; } bool CNetServerWorker::OnSyncCheck(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_SYNC_CHECK); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CSyncCheckMessage* message = (CSyncCheckMessage*)event->GetParamRef(); server.m_ServerTurnManager->NotifyFinishedClientUpdate(*session, message->m_Turn, message->m_Hash); return true; } bool CNetServerWorker::OnEndCommandBatch(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_END_COMMAND_BATCH); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CEndCommandBatchMessage* message = (CEndCommandBatchMessage*)event->GetParamRef(); // The turn-length field is ignored server.m_ServerTurnManager->NotifyFinishedClientCommands(*session, message->m_Turn); return true; } bool CNetServerWorker::OnChat(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CHAT); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CChatMessage* message = (CChatMessage*)event->GetParamRef(); message->m_GUID = session->GetGUID(); server.Broadcast(message, { NSS_PREGAME, NSS_INGAME }); return true; } bool CNetServerWorker::OnReady(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_READY); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); // Occurs if a client presses not-ready // in the very last moment before the hosts starts the game if (server.m_State == SERVER_STATE_LOADING) return true; CReadyMessage* message = (CReadyMessage*)event->GetParamRef(); message->m_GUID = session->GetGUID(); server.Broadcast(message, { NSS_PREGAME }); server.m_PlayerAssignments[message->m_GUID].m_Status = message->m_Status; return true; } bool CNetServerWorker::OnClearAllReady(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CLEAR_ALL_READY); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); if (session->GetGUID() == server.m_ControllerGUID) server.ClearAllPlayerReady(); return true; } bool CNetServerWorker::OnGameSetup(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_GAME_SETUP); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); // Changing the settings after gamestart is not implemented and would cause an Out-of-sync error. // This happened when doubleclicking on the startgame button. if (server.m_State != SERVER_STATE_PREGAME) return true; // Only the controller is allowed to send game setup updates. // TODO: it would be good to allow other players to request changes to some settings, // e.g. their civilisation. // Possibly this should use another message, to enforce a single source of truth. if (session->GetGUID() == server.m_ControllerGUID) { CGameSetupMessage* message = (CGameSetupMessage*)event->GetParamRef(); server.Broadcast(message, { NSS_PREGAME }); } return true; } bool CNetServerWorker::OnAssignPlayer(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_ASSIGN_PLAYER); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); if (session->GetGUID() == server.m_ControllerGUID) { CAssignPlayerMessage* message = (CAssignPlayerMessage*)event->GetParamRef(); server.AssignPlayer(message->m_PlayerID, message->m_GUID); } return true; } bool CNetServerWorker::OnGameStart(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_GAME_START); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); if (session->GetGUID() != server.m_ControllerGUID) return true; CGameStartMessage* message = (CGameStartMessage*)event->GetParamRef(); server.StartGame(message->m_InitAttributes); return true; } bool CNetServerWorker::OnLoadedGame(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_LOADED_GAME); CNetServerSession* loadedSession = (CNetServerSession*)context; CNetServerWorker& server = loadedSession->GetServer(); // We're in the loading state, so wait until every client has loaded // before starting the game ENSURE(server.m_State == SERVER_STATE_LOADING); if (server.CheckGameLoadStatus(loadedSession)) return true; CClientsLoadingMessage message; // We always send all GUIDs of clients in the loading state // so that we don't have to bother about switching GUI pages for (CNetServerSession* session : server.m_Sessions) if (session->GetCurrState() != NSS_INGAME && loadedSession->GetGUID() != session->GetGUID()) { CClientsLoadingMessage::S_m_Clients client; client.m_GUID = session->GetGUID(); message.m_Clients.push_back(client); } // Send to the client who has loaded the game but did not reach the NSS_INGAME state yet loadedSession->SendMessage(&message); server.Broadcast(&message, { NSS_INGAME }); return true; } bool CNetServerWorker::OnJoinSyncingLoadedGame(void* context, CFsmEvent* event) { // A client rejoining an in-progress game has now finished loading the // map and deserialized the initial state. // The simulation may have progressed since then, so send any subsequent // commands to them and set them as an active player so they can participate // in all future turns. // // (TODO: if it takes a long time for them to receive and execute all these // commands, the other players will get frozen for that time and may be unhappy; // we could try repeating this process a few times until the client converges // on the up-to-date state, before setting them as active.) ENSURE(event->GetType() == (uint)NMT_LOADED_GAME); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CLoadedGameMessage* message = (CLoadedGameMessage*)event->GetParamRef(); u32 turn = message->m_CurrentTurn; u32 readyTurn = server.m_ServerTurnManager->GetReadyTurn(); // Send them all commands received since their saved state, // and turn-ended messages for any turns that have already been processed for (size_t i = turn + 1; i < std::max(readyTurn+1, (u32)server.m_SavedCommands.size()); ++i) { if (i < server.m_SavedCommands.size()) for (size_t j = 0; j < server.m_SavedCommands[i].size(); ++j) session->SendMessage(&server.m_SavedCommands[i][j]); if (i <= readyTurn) { CEndCommandBatchMessage endMessage; endMessage.m_Turn = i; endMessage.m_TurnLength = server.m_ServerTurnManager->GetSavedTurnLength(i); session->SendMessage(&endMessage); } } // Tell the turn manager to expect commands from this new client // Special case: the controller shouldn't be treated as an observer in any case. bool isObserver = server.m_PlayerAssignments[session->GetGUID()].m_PlayerID == -1 && server.m_ControllerGUID != session->GetGUID(); server.m_ServerTurnManager->InitialiseClient(session->GetHostID(), readyTurn, isObserver); // Tell the client that everything has finished loading and it should start now CLoadedGameMessage loaded; loaded.m_CurrentTurn = readyTurn; session->SendMessage(&loaded); return true; } bool CNetServerWorker::OnRejoined(void* context, CFsmEvent* event) { // A client has finished rejoining and the loading screen disappeared. ENSURE(event->GetType() == (uint)NMT_REJOINED); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); // Inform everyone of the client having rejoined CRejoinedMessage* message = (CRejoinedMessage*)event->GetParamRef(); message->m_GUID = session->GetGUID(); server.Broadcast(message, { NSS_INGAME }); // Send all pausing players to the rejoined client. for (const CStr& guid : server.m_PausingPlayers) { CClientPausedMessage pausedMessage; pausedMessage.m_GUID = guid; pausedMessage.m_Pause = true; session->SendMessage(&pausedMessage); } return true; } bool CNetServerWorker::OnKickPlayer(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_KICKED); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); if (session->GetGUID() == server.m_ControllerGUID) { CKickedMessage* message = (CKickedMessage*)event->GetParamRef(); server.KickPlayer(message->m_Name, message->m_Ban); } return true; } bool CNetServerWorker::OnDisconnect(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CONNECTION_LOST); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); server.OnUserLeave(session); return true; } bool CNetServerWorker::OnClientPaused(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CLIENT_PAUSED); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CClientPausedMessage* message = (CClientPausedMessage*)event->GetParamRef(); message->m_GUID = session->GetGUID(); // Update the list of pausing players. std::vector::iterator player = std::find(server.m_PausingPlayers.begin(), server.m_PausingPlayers.end(), session->GetGUID()); if (message->m_Pause) { if (player != server.m_PausingPlayers.end()) return true; server.m_PausingPlayers.push_back(session->GetGUID()); } else { if (player == server.m_PausingPlayers.end()) return true; server.m_PausingPlayers.erase(player); } // Send messages to clients that are in game, and are not the client who paused. for (CNetServerSession* netSession : server.m_Sessions) if (netSession->GetCurrState() == NSS_INGAME && message->m_GUID != netSession->GetGUID()) netSession->SendMessage(message); return true; } bool CNetServerWorker::CheckGameLoadStatus(CNetServerSession* changedSession) { for (const CNetServerSession* session : m_Sessions) if (session != changedSession && session->GetCurrState() != NSS_INGAME) return false; // Inform clients that everyone has loaded the map and that the game can start CLoadedGameMessage loaded; loaded.m_CurrentTurn = 0; // Notice the changedSession is still in the NSS_PREGAME state Broadcast(&loaded, { NSS_PREGAME, NSS_INGAME }); m_State = SERVER_STATE_INGAME; return true; } void CNetServerWorker::StartGame(const CStr& initAttribs) { for (std::pair& player : m_PlayerAssignments) if (player.second.m_Enabled && player.second.m_PlayerID != -1 && player.second.m_Status == 0) { LOGERROR("Tried to start the game without player \"%s\" being ready!", utf8_from_wstring(player.second.m_Name).c_str()); return; } m_ServerTurnManager = new CNetServerTurnManager(*this); for (CNetServerSession* session : m_Sessions) { // Special case: the controller shouldn't be treated as an observer in any case. bool isObserver = m_PlayerAssignments[session->GetGUID()].m_PlayerID == -1 && m_ControllerGUID != session->GetGUID(); m_ServerTurnManager->InitialiseClient(session->GetHostID(), 0, isObserver); } m_State = SERVER_STATE_LOADING; // Remove players and observers that are not present when the game starts for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end();) if (it->second.m_Enabled) ++it; else it = m_PlayerAssignments.erase(it); SendPlayerAssignments(); // Update init attributes. They should no longer change. Script::ParseJSON(ScriptRequest(m_ScriptInterface), initAttribs, &m_InitAttributes); CGameStartMessage gameStart; gameStart.m_InitAttributes = initAttribs; Broadcast(&gameStart, { NSS_PREGAME }); } CStrW CNetServerWorker::SanitisePlayerName(const CStrW& original) { const size_t MAX_LENGTH = 32; CStrW name = original; name.Replace(L"[", L"{"); // remove GUI tags name.Replace(L"]", L"}"); // remove for symmetry // Restrict the length if (name.length() > MAX_LENGTH) name = name.Left(MAX_LENGTH); // Don't allow surrounding whitespace name.Trim(PS_TRIM_BOTH); // Don't allow empty name if (name.empty()) name = L"Anonymous"; return name; } CStrW CNetServerWorker::DeduplicatePlayerName(const CStrW& original) { CStrW name = original; // Try names "Foo", "Foo (2)", "Foo (3)", etc size_t id = 2; while (true) { bool unique = true; for (const CNetServerSession* session : m_Sessions) { if (session->GetUserName() == name) { unique = false; break; } } if (unique) return name; name = original + L" (" + CStrW::FromUInt(id++) + L")"; } } void CNetServerWorker::SendHolePunchingMessage(const CStr& ipStr, u16 port) { if (m_Host) StunClient::SendHolePunchingMessages(*m_Host, ipStr, port); } CNetServer::CNetServer(bool useLobbyAuth) : m_Worker(new CNetServerWorker(useLobbyAuth)), m_LobbyAuth(useLobbyAuth), m_UseSTUN(false), m_PublicIp(""), m_PublicPort(20595), m_Password() { } CNetServer::~CNetServer() { delete m_Worker; } bool CNetServer::GetUseSTUN() const { return m_UseSTUN; } bool CNetServer::UseLobbyAuth() const { return m_LobbyAuth; } bool CNetServer::SetupConnection(const u16 port) { return m_Worker->SetupConnection(port); } CStr CNetServer::GetPublicIp() const { return m_PublicIp; } u16 CNetServer::GetPublicPort() const { return m_PublicPort; } u16 CNetServer::GetLocalPort() const { std::lock_guard lock(m_Worker->m_WorkerMutex); if (!m_Worker->m_Host) return 0; return m_Worker->m_Host->address.port; } void CNetServer::SetConnectionData(const CStr& ip, const u16 port) { m_PublicIp = ip; m_PublicPort = port; m_UseSTUN = false; } bool CNetServer::SetConnectionDataViaSTUN() { m_UseSTUN = true; std::lock_guard lock(m_Worker->m_WorkerMutex); if (!m_Worker->m_Host) return false; return StunClient::FindPublicIP(*m_Worker->m_Host, m_PublicIp, m_PublicPort); } bool CNetServer::CheckPasswordAndIncrement(const std::string& username, const std::string& password, const std::string& salt) { std::unordered_map::iterator it = m_FailedAttempts.find(username); if (m_Worker->CheckPassword(password, salt)) { if (it != m_FailedAttempts.end()) it->second = 0; return true; } if (it == m_FailedAttempts.end()) m_FailedAttempts.emplace(username, 1); else it->second++; return false; } bool CNetServer::IsBanned(const std::string& username) const { std::unordered_map::const_iterator it = m_FailedAttempts.find(username); return it != m_FailedAttempts.end() && it->second >= FAILED_PASSWORD_TRIES_BEFORE_BAN; } void CNetServer::SetPassword(const CStr& password) { m_Password = password; std::lock_guard lock(m_Worker->m_WorkerMutex); m_Worker->SetPassword(password); } void CNetServer::SetControllerSecret(const std::string& secret) { std::lock_guard lock(m_Worker->m_WorkerMutex); m_Worker->SetControllerSecret(secret); } void CNetServer::StartGame() { std::lock_guard lock(m_Worker->m_WorkerMutex); m_Worker->m_StartGameQueue.push_back(true); } void CNetServer::UpdateInitAttributes(JS::MutableHandleValue attrs, const ScriptRequest& rq) { // Pass the attributes as JSON, since that's the easiest safe // cross-thread way of passing script data std::string attrsJSON = Script::StringifyJSON(rq, attrs, false); std::lock_guard lock(m_Worker->m_WorkerMutex); m_Worker->m_InitAttributesQueue.push_back(attrsJSON); } void CNetServer::OnLobbyAuth(const CStr& name, const CStr& token) { std::lock_guard lock(m_Worker->m_WorkerMutex); m_Worker->m_LobbyAuthQueue.push_back(std::make_pair(name, token)); } void CNetServer::SetTurnLength(u32 msecs) { std::lock_guard lock(m_Worker->m_WorkerMutex); m_Worker->m_TurnLengthQueue.push_back(msecs); } void CNetServer::SendHolePunchingMessage(const CStr& ip, u16 port) { m_Worker->SendHolePunchingMessage(ip, port); } Index: ps/trunk/source/ps/Game.cpp =================================================================== --- ps/trunk/source/ps/Game.cpp (revision 27944) +++ ps/trunk/source/ps/Game.cpp (revision 27945) @@ -1,477 +1,477 @@ /* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "Game.h" #include "graphics/GameView.h" #include "graphics/LOSTexture.h" #include "graphics/ParticleManager.h" #include "graphics/UnitManager.h" #include "gui/GUIManager.h" #include "gui/CGUI.h" #include "lib/config2.h" #include "lib/timer.h" #include "network/NetClient.h" #include "network/NetServer.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/GameSetup/GameSetup.h" #include "ps/Loader.h" #include "ps/Profile.h" #include "ps/Replay.h" #include "ps/World.h" #include "ps/VideoMode.h" #include "renderer/Renderer.h" #include "renderer/SceneRenderer.h" #include "renderer/TimeManager.h" #include "renderer/WaterManager.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/JSON.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpPlayer.h" #include "simulation2/components/ICmpPlayerManager.h" #include "simulation2/system/ReplayTurnManager.h" #include "soundmanager/ISoundManager.h" #include "tools/atlas/GameInterface/GameLoop.h" #include extern GameLoopState* g_AtlasGameLoop; /** * Globally accessible pointer to the CGame object. **/ CGame *g_Game=NULL; const CStr CGame::EventNameSimulationUpdate = "SimulationUpdate"; /** * Constructor * **/ CGame::CGame(bool replayLog): m_World(new CWorld(*this)), m_Simulation2(new CSimulation2(&m_World->GetUnitManager(), g_ScriptContext, &m_World->GetTerrain())), // TODO: we need to remove that global dependency. Maybe the game view // should be created outside only if needed. m_GameView(CRenderer::IsInitialised() ? new CGameView(g_VideoMode.GetBackendDevice(), this) : nullptr), m_GameStarted(false), m_Paused(false), m_SimRate(1.0f), m_PlayerID(-1), m_ViewedPlayerID(-1), m_IsSavedGame(false), m_IsVisualReplay(false), m_ReplayStream(NULL) { // TODO: should use CDummyReplayLogger unless activated by cmd-line arg, perhaps? if (replayLog) m_ReplayLogger = new CReplayLogger(m_Simulation2->GetScriptInterface()); else m_ReplayLogger = new CDummyReplayLogger(); // Need to set the CObjectManager references after various objects have // been initialised, so do it here rather than via the initialisers above. if (m_GameView) m_World->GetUnitManager().SetObjectManager(m_GameView->GetObjectManager()); m_TurnManager = new CLocalTurnManager(*m_Simulation2, GetReplayLogger()); // this will get replaced if we're a net server/client m_Simulation2->LoadDefaultScripts(); } /** * Destructor * **/ CGame::~CGame() { // Again, the in-game call tree is going to be different to the main menu one. if (CProfileManager::IsInitialised()) g_Profiler.StructuralReset(); if (m_ReplayLogger && m_GameStarted) m_ReplayLogger->SaveMetadata(*m_Simulation2); delete m_TurnManager; delete m_GameView; delete m_Simulation2; delete m_World; delete m_ReplayLogger; delete m_ReplayStream; } void CGame::SetTurnManager(CTurnManager* turnManager) { if (m_TurnManager) delete m_TurnManager; m_TurnManager = turnManager; if (m_TurnManager) m_TurnManager->SetPlayerID(m_PlayerID); } int CGame::LoadVisualReplayData() { ENSURE(m_IsVisualReplay); ENSURE(!m_ReplayPath.empty()); ENSURE(m_ReplayStream); CReplayTurnManager* replayTurnMgr = static_cast(GetTurnManager()); u32 currentTurn = 0; std::string type; while ((*m_ReplayStream >> type).good()) { if (type == "turn") { u32 turn = 0; u32 turnLength = 0; *m_ReplayStream >> turn >> turnLength; ENSURE(turn == currentTurn && "You tried to replay a commands.txt file of a rejoined client. Please use the host's file."); replayTurnMgr->StoreReplayTurnLength(currentTurn, turnLength); } else if (type == "cmd") { player_id_t player; *m_ReplayStream >> player; std::string line; std::getline(*m_ReplayStream, line); replayTurnMgr->StoreReplayCommand(currentTurn, player, line); } else if (type == "hash" || type == "hash-quick") { bool quick = (type == "hash-quick"); std::string replayHash; *m_ReplayStream >> replayHash; replayTurnMgr->StoreReplayHash(currentTurn, replayHash, quick); } else if (type == "end") ++currentTurn; else CancelLoad(L"Failed to load replay data (unrecognized content)"); } SAFE_DELETE(m_ReplayStream); m_FinalReplayTurn = currentTurn > 0 ? currentTurn - 1 : 0; replayTurnMgr->StoreFinalReplayTurn(m_FinalReplayTurn); return 0; } bool CGame::StartVisualReplay(const OsPath& replayPath) { debug_printf("Starting to replay %s\n", replayPath.string8().c_str()); m_IsVisualReplay = true; SetTurnManager(new CReplayTurnManager(*m_Simulation2, GetReplayLogger())); m_ReplayPath = replayPath; m_ReplayStream = new std::ifstream(OsString(replayPath).c_str()); std::string type; ENSURE((*m_ReplayStream >> type).good() && type == "start"); std::string line; std::getline(*m_ReplayStream, line); const ScriptInterface& scriptInterface = m_Simulation2->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue attribs(rq.cx); Script::ParseJSON(rq, line, &attribs); StartGame(&attribs, ""); return true; } /** * Initializes the game with the set of attributes provided. * Makes calls to initialize the game view, world, and simulation objects. * Calls are made to facilitate progress reporting of the initialization. **/ void CGame::RegisterInit(const JS::HandleValue attribs, const std::string& savedState) { const ScriptInterface& scriptInterface = m_Simulation2->GetScriptInterface(); ScriptRequest rq(scriptInterface); m_IsSavedGame = !savedState.empty(); m_Simulation2->SetInitAttributes(attribs); std::string mapType; Script::GetProperty(rq, attribs, "mapType", mapType); float speed; if (Script::HasProperty(rq, attribs, "gameSpeed")) { if (Script::GetProperty(rq, attribs, "gameSpeed", speed)) SetSimRate(speed); else LOGERROR("GameSpeed could not be parsed."); } LDR_BeginRegistering(); LDR_Register([this](const double) { return m_Simulation2->ProgressiveLoad(); }, L"Simulation init", 1000); // RC, 040804 - GameView needs to be initialized before World, otherwise GameView initialization // overwrites anything stored in the map file that gets loaded by CWorld::Initialize with default // values. At the minute, it's just lighting settings, but could be extended to store camera position. // Storing lighting settings in the game view seems a little odd, but it's no big deal; maybe move it at // some point to be stored in the world object? if (m_GameView) m_GameView->RegisterInit(); if (mapType == "random") { // Load random map attributes std::wstring scriptFile; JS::RootedValue settings(rq.cx); Script::GetProperty(rq, attribs, "script", scriptFile); Script::GetProperty(rq, attribs, "settings", &settings); - m_World->RegisterInitRMS(scriptFile, *scriptInterface.GetContext(), settings, m_PlayerID); + m_World->RegisterInitRMS(scriptFile, scriptInterface.GetContext(), settings, m_PlayerID); } else { std::wstring mapFile; JS::RootedValue settings(rq.cx); Script::GetProperty(rq, attribs, "map", mapFile); Script::GetProperty(rq, attribs, "settings", &settings); - m_World->RegisterInit(mapFile, *scriptInterface.GetContext(), settings, m_PlayerID); + m_World->RegisterInit(mapFile, scriptInterface.GetContext(), settings, m_PlayerID); } if (m_GameView) LDR_Register([&waterManager = g_Renderer.GetSceneRenderer().GetWaterManager()](const double) { return waterManager.LoadWaterTextures(); }, L"LoadWaterTextures", 80); if (m_IsSavedGame) LDR_Register([this, savedState](const double) { return LoadInitialState(savedState); }, L"Loading game", 1000); if (m_IsVisualReplay) LDR_Register([this](const double) { return LoadVisualReplayData(); }, L"Loading visual replay data", 1000); LDR_EndRegistering(); } int CGame::LoadInitialState(const std::string& savedState) { ENSURE(m_IsSavedGame); std::stringstream stream(savedState); bool ok = m_Simulation2->DeserializeState(stream); if (!ok) { CancelLoad(L"Failed to load saved game state. It might have been\nsaved with an incompatible version of the game."); return 0; } return 0; } /** * Game initialization has been completed. Set game started flag and start the session. * * @return PSRETURN 0 **/ PSRETURN CGame::ReallyStartGame() { // Call the script function InitGame only for new games, not saved games if (!m_IsSavedGame) { // Perform some simulation initializations (replace skirmish entities, explore territories, etc.) // that needs to be done before setting up the AI and shouldn't be done in Atlas if (!g_AtlasGameLoop->running) m_Simulation2->PreInitGame(); m_Simulation2->InitGame(); } // We need to do an initial Interpolate call to set up all the models etc, // because Update might never interpolate (e.g. if the game starts paused) // and we could end up rendering before having set up any models (so they'd // all be invisible) Interpolate(0, 0); m_GameStarted = true; // Preload resources to avoid blinking on a first game frame. if (CRenderer::IsInitialised()) g_Renderer.PreloadResourcesBeforeNextFrame(); if (g_NetClient) g_NetClient->LoadFinished(); // Call the reallyStartGame GUI function, but only if it exists if (g_GUI && g_GUI->GetPageCount()) { std::shared_ptr scriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue global(rq.cx, rq.globalValue()); if (Script::HasProperty(rq, global, "reallyStartGame")) ScriptFunction::CallVoid(rq, global, "reallyStartGame"); } debug_printf("GAME STARTED, ALL INIT COMPLETE\n"); // The call tree we've built for pregame probably isn't useful in-game. if (CProfileManager::IsInitialised()) g_Profiler.StructuralReset(); return 0; } int CGame::GetPlayerID() { return m_PlayerID; } void CGame::SetPlayerID(player_id_t playerID) { m_PlayerID = playerID; m_ViewedPlayerID = playerID; if (m_TurnManager) m_TurnManager->SetPlayerID(m_PlayerID); } int CGame::GetViewedPlayerID() { return m_ViewedPlayerID; } void CGame::SetViewedPlayerID(player_id_t playerID) { m_ViewedPlayerID = playerID; } void CGame::StartGame(JS::MutableHandleValue attribs, const std::string& savedState) { if (m_ReplayLogger) m_ReplayLogger->StartGame(attribs); RegisterInit(attribs, savedState); } // TODO: doInterpolate is optional because Atlas interpolates explicitly, // so that it has more control over the update rate. The game might want to // do the same, and then doInterpolate should be redundant and removed. void CGame::Update(const double deltaRealTime, bool doInterpolate) { if (m_Paused || !m_TurnManager) return; const double deltaSimTime = deltaRealTime * m_SimRate; if (deltaSimTime) { // At the normal sim rate, we currently want to render at least one // frame per simulation turn, so let maxTurns be 1. But for fast-forward // sim rates we want to allow more, so it's not bounded by framerate, // so just use the sim rate itself as the number of turns per frame. size_t maxTurns = (size_t)m_SimRate; if (m_TurnManager->Update(deltaSimTime, maxTurns)) { { PROFILE3("gui sim update"); g_GUI->SendEventToAll(EventNameSimulationUpdate); } GetView()->GetLOSTexture().MakeDirty(); } if (CRenderer::IsInitialised()) g_Renderer.GetTimeManager().Update(deltaSimTime); } if (doInterpolate) m_TurnManager->Interpolate(deltaSimTime, deltaRealTime); } void CGame::Interpolate(float simFrameLength, float realFrameLength) { if (!m_TurnManager) return; m_TurnManager->Interpolate(simFrameLength, realFrameLength); } static CColor BrokenColor(0.3f, 0.3f, 0.3f, 1.0f); void CGame::CachePlayerColors() { m_PlayerColors.clear(); CmpPtr cmpPlayerManager(*m_Simulation2, SYSTEM_ENTITY); if (!cmpPlayerManager) return; int numPlayers = cmpPlayerManager->GetNumPlayers(); m_PlayerColors.resize(numPlayers); for (int i = 0; i < numPlayers; ++i) { CmpPtr cmpPlayer(*m_Simulation2, cmpPlayerManager->GetPlayerByID(i)); if (!cmpPlayer) m_PlayerColors[i] = BrokenColor; else m_PlayerColors[i] = cmpPlayer->GetDisplayedColor(); } } const CColor& CGame::GetPlayerColor(player_id_t player) const { if (player < 0 || player >= (int)m_PlayerColors.size()) return BrokenColor; return m_PlayerColors[player]; } bool CGame::IsGameFinished() const { for (const std::pair& p : m_Simulation2->GetEntitiesWithInterface(IID_Player)) { CmpPtr cmpPlayer(*m_Simulation2, p.first); if (cmpPlayer && cmpPlayer->GetState() == "won") return true; } return false; } Index: ps/trunk/source/scriptinterface/ScriptInterface.cpp =================================================================== --- ps/trunk/source/scriptinterface/ScriptInterface.cpp (revision 27944) +++ ps/trunk/source/scriptinterface/ScriptInterface.cpp (revision 27945) @@ -1,717 +1,719 @@ /* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ScriptInterface.h" #include "lib/debug.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Profile.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/Object.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptExtraHeaders.h" #include "scriptinterface/ScriptStats.h" #include "scriptinterface/StructuredClone.h" #include #include #define BOOST_MULTI_INDEX_DISABLE_SERIALIZATION #include #include #include #include #include #include #include /** * @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 std::shared_ptr& context, JS::Compartment* compartment); + ScriptInterface_impl(const char* nativeScopeName, ScriptContext& context, JS::Compartment* compartment); ~ScriptInterface_impl(); // Take care to keep this declaration before heap rooted members. Destructors of heap rooted // members have to be called before the context destructor. - std::shared_ptr m_context; + ScriptContext& m_context; friend ScriptRequest; private: JSContext* m_cx; JS::PersistentRootedObject m_glob; // global scope object public: boost::rand48* m_rng; JS::PersistentRootedObject m_nativeScope; // native function scope object }; /** * Constructor for ScriptRequest - here because it needs access into ScriptInterface_impl. */ ScriptRequest::ScriptRequest(const ScriptInterface& scriptInterface) : cx(scriptInterface.m->m_cx), glob(scriptInterface.m->m_glob), nativeScope(scriptInterface.m->m_nativeScope), m_ScriptInterface(scriptInterface) { m_FormerRealm = JS::EnterRealm(cx, scriptInterface.m->m_glob); } ScriptRequest::~ScriptRequest() { JS::LeaveRealm(cx, m_FormerRealm); } ScriptRequest::ScriptRequest(JSContext* cx) : ScriptRequest(ScriptInterface::CmptPrivate::GetScriptInterface(cx)) { } JS::Value ScriptRequest::globalValue() const { return JS::ObjectValue(*glob); } const ScriptInterface& ScriptRequest::GetScriptInterface() const { return m_ScriptInterface; } namespace { JSClassOps global_classops = { nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, JS_GlobalObjectTraceHook }; JSClass global_class = { "global", JSCLASS_GLOBAL_FLAGS, &global_classops }; // Functions in the global namespace: bool print(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); ScriptRequest rq(cx); for (uint i = 0; i < args.length(); ++i) { std::wstring str; if (!Script::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; } ScriptRequest rq(cx); std::wstring str; if (!Script::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; } ScriptRequest rq(cx); std::wstring str; if (!Script::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; } ScriptRequest rq(cx); std::wstring str; if (!Script::FromJSVal(rq, args[0], str)) return false; LOGERROR("%s", utf8_from_wstring(str)); args.rval().setUndefined(); return true; } JS::Value deepcopy(const ScriptRequest& rq, JS::HandleValue val) { if (val.isNullOrUndefined()) { ScriptException::Raise(rq, "deepcopy requires one argument."); return JS::UndefinedValue(); } JS::RootedValue ret(rq.cx, Script::DeepCopy(rq, val)); if (ret.isNullOrUndefined()) { ScriptException::Raise(rq, "deepcopy StructureClone copy failed."); return JS::UndefinedValue(); } return ret; } JS::Value deepfreeze(const ScriptInterface& scriptInterface, JS::HandleValue val) { ScriptRequest rq(scriptInterface); if (!val.isObject()) { ScriptException::Raise(rq, "deepfreeze requires exactly one object as an argument."); return JS::UndefinedValue(); } Script::FreezeObject(rq, val, true); return val; } void ProfileStart(const std::string& regionName) { const char* name = "(ProfileStart)"; typedef boost::flyweight< std::string, boost::flyweights::no_tracking, boost::flyweights::no_locking > StringFlyweight; if (!regionName.empty()) name = StringFlyweight(regionName).get().c_str(); if (CProfileManager::IsInitialised() && Threading::IsMainThread()) g_Profiler.StartScript(name); g_Profiler2.RecordRegionEnter(name); } void ProfileStop() { if (CProfileManager::IsInitialised() && Threading::IsMainThread()) g_Profiler.Stop(); g_Profiler2.RecordRegionLeave(); } void ProfileAttribute(const std::string& attr) { const char* name = "(ProfileAttribute)"; typedef boost::flyweight< std::string, boost::flyweights::no_tracking, boost::flyweights::no_locking > StringFlyweight; if (!attr.empty()) name = StringFlyweight(attr).get().c_str(); g_Profiler2.RecordAttribute("%s", name); } // 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; } } } // anonymous namespace bool ScriptInterface::MathRandom(double& nbr) const { if (m->m_rng == nullptr) return false; nbr = generate_uniform_real(*(m->m_rng), 0.0, 1.0); return true; } bool ScriptInterface::Math_random(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); double r; if (!ScriptInterface::CmptPrivate::GetScriptInterface(cx).MathRandom(r)) return false; args.rval().setNumber(r); return true; } -ScriptInterface_impl::ScriptInterface_impl(const char* nativeScopeName, const std::shared_ptr& context, JS::Compartment* compartment) : - m_context(context), m_cx(context->GetGeneralJSContext()), m_glob(context->GetGeneralJSContext()), m_nativeScope(context->GetGeneralJSContext()) +ScriptInterface_impl::ScriptInterface_impl(const char* nativeScopeName, ScriptContext& context, + JS::Compartment* compartment) : + m_context(context), m_cx(context.GetGeneralJSContext()), m_glob(context.GetGeneralJSContext()), + m_nativeScope(context.GetGeneralJSContext()) { JS::RealmCreationOptions creationOpt; // Keep JIT code during non-shrinking GCs. This brings a quite big performance improvement. creationOpt.setPreserveJitCode(true); // Enable uneval creationOpt.setToSourceEnabled(true); if (compartment) creationOpt.setExistingCompartment(compartment); else // This is the default behaviour. creationOpt.setNewCompartmentAndZone(); JS::RealmOptions opt(creationOpt, JS::RealmBehaviors{}); m_glob = JS_NewGlobalObject(m_cx, &global_class, nullptr, JS::OnNewGlobalHookOption::FireOnNewGlobalHook, opt); JSAutoRealm autoRealm(m_cx, m_glob); ENSURE(JS::InitRealmStandardClasses(m_cx)); JS_DefineProperty(m_cx, m_glob, "global", m_glob, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); // These first 4 actually use CallArgs & thus don't use ScriptFunction 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); ScriptFunction::Register(m_cx, m_glob, "clone"); ScriptFunction::Register(m_cx, m_glob, "deepfreeze"); m_nativeScope = JS_DefineObject(m_cx, m_glob, nativeScopeName, nullptr, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); ScriptFunction::Register<&ProfileStart>(m_cx, m_nativeScope, "ProfileStart"); ScriptFunction::Register<&ProfileStop>(m_cx, m_nativeScope, "ProfileStop"); ScriptFunction::Register<&ProfileAttribute>(m_cx, m_nativeScope, "ProfileAttribute"); - m_context->RegisterRealm(JS::GetObjectRealmOrNull(m_glob)); + m_context.RegisterRealm(JS::GetObjectRealmOrNull(m_glob)); } ScriptInterface_impl::~ScriptInterface_impl() { - m_context->UnRegisterRealm(JS::GetObjectRealmOrNull(m_glob)); + m_context.UnRegisterRealm(JS::GetObjectRealmOrNull(m_glob)); } -ScriptInterface::ScriptInterface(const char* nativeScopeName, const char* debugName, const std::shared_ptr& context) : +ScriptInterface::ScriptInterface(const char* nativeScopeName, const char* debugName, ScriptContext& context) : m(std::make_unique(nativeScopeName, context, nullptr)) { // Profiler stats table isn't thread-safe, so only enable this on the main thread if (Threading::IsMainThread()) { if (g_ScriptStatsTable) g_ScriptStatsTable->Add(this, debugName); } ScriptRequest rq(this); m_CmptPrivate.pScriptInterface = this; JS::SetRealmPrivate(JS::GetObjectRealmOrNull(rq.glob), (void*)&m_CmptPrivate); } ScriptInterface::ScriptInterface(const char* nativeScopeName, const char* debugName, const ScriptInterface& neighbor) { ScriptRequest nrq(neighbor); JS::Compartment* comp = JS::GetCompartmentForRealm(JS::GetCurrentRealmOrNull(nrq.cx)); m = std::make_unique(nativeScopeName, neighbor.GetContext(), comp); // Profiler stats table isn't thread-safe, so only enable this on the main thread if (Threading::IsMainThread()) { if (g_ScriptStatsTable) g_ScriptStatsTable->Add(this, debugName); } ScriptRequest rq(this); m_CmptPrivate.pScriptInterface = this; JS::SetRealmPrivate(JS::GetObjectRealmOrNull(rq.glob), (void*)&m_CmptPrivate); } ScriptInterface::~ScriptInterface() { if (Threading::IsMainThread()) { if (g_ScriptStatsTable) g_ScriptStatsTable->Remove(this); } } const ScriptInterface& ScriptInterface::CmptPrivate::GetScriptInterface(JSContext *cx) { CmptPrivate* pCmptPrivate = (CmptPrivate*)JS::GetRealmPrivate(JS::GetCurrentRealmOrNull(cx)); ENSURE(pCmptPrivate); return *pCmptPrivate->pScriptInterface; } void* ScriptInterface::CmptPrivate::GetCBData(JSContext *cx) { CmptPrivate* pCmptPrivate = (CmptPrivate*)JS::GetRealmPrivate(JS::GetCurrentRealmOrNull(cx)); return pCmptPrivate ? pCmptPrivate->pCBData : nullptr; } void ScriptInterface::SetCallbackData(void* pCBData) { m_CmptPrivate.pCBData = pCBData; } template <> void* ScriptInterface::ObjectFromCBData(const ScriptRequest& rq) { return ScriptInterface::CmptPrivate::GetCBData(rq.cx); } 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) { ScriptRequest 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; } } ScriptException::CatchPending(rq); LOGERROR("ReplaceNondeterministicRNG: failed to replace Math.random"); return false; } JSContext* ScriptInterface::GetGeneralJSContext() const { - return m->m_context->GetGeneralJSContext(); + return m->m_context.GetGeneralJSContext(); } -std::shared_ptr ScriptInterface::GetContext() const +ScriptContext& ScriptInterface::GetContext() const { return m->m_context; } void ScriptInterface::CallConstructor(JS::HandleValue ctor, JS::HandleValueArray argv, JS::MutableHandleValue out) const { ScriptRequest rq(this); if (!ctor.isObject()) { LOGERROR("CallConstructor: ctor is not an object"); out.setNull(); return; } JS::RootedObject objOut(rq.cx); if (!JS::Construct(rq.cx, ctor, argv, &objOut)) out.setNull(); else out.setObjectOrNull(objOut); } void ScriptInterface::DefineCustomObjectType(JSClass *clasp, JSNative constructor, uint minArgs, JSPropertySpec *ps, JSFunctionSpec *fs, JSPropertySpec *static_ps, JSFunctionSpec *static_fs) { ScriptRequest 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 == nullptr) { ScriptException::CatchPending(rq); 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(); ScriptRequest rq(this); JS::RootedObject prototype(rq.cx, it->second.m_Prototype.get()); return JS_NewObjectWithGivenProto(rq.cx, it->second.m_Class, prototype); } bool ScriptInterface::SetGlobal_(const char* name, JS::HandleValue value, bool replace, bool constant, bool enumerate) { ScriptRequest 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) || !desc.isSome()) return false; if (!desc->writable()) { if (!replace) { ScriptException::Raise(rq, "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()) { ScriptException::Raise(rq, "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::GetGlobalProperty(const ScriptRequest& rq, const std::string& name, JS::MutableHandleValue out) { // Try to get the object as a property of the global object. JS::RootedObject global(rq.cx, rq.glob); if (!JS_GetProperty(rq.cx, global, name.c_str(), out)) { out.set(JS::NullHandleValue); return false; } if (!out.isNullOrUndefined()) return true; // Some objects, such as const definitions, or Class definitions, are hidden inside closures. // We must fetch those from the correct lexical scope. //JS::RootedValue glob(cx); JS::RootedObject lexical_environment(rq.cx, JS_GlobalLexicalEnvironment(rq.glob)); if (!JS_GetProperty(rq.cx, lexical_environment, name.c_str(), out)) { out.set(JS::NullHandleValue); return false; } if (!out.isNullOrUndefined()) return true; out.set(JS::NullHandleValue); return false; } bool ScriptInterface::SetPrototype(JS::HandleValue objVal, JS::HandleValue protoVal) { ScriptRequest 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::LoadScript(const VfsPath& filename, const std::string& code) const { ScriptRequest rq(this); JS::RootedObject global(rq.cx, rq.glob); // 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); // Set the line to 0 because CompileFunction silently adds a `(function() {` as the first line, // and errors get misreported. // TODO: it would probably be better to not implicitly introduce JS scopes. options.setFileAndLine(filenameStr.c_str(), 0); options.setIsRunOnce(false); JS::SourceText src; ENSURE(src.init(rq.cx, code.c_str(), code.length(), JS::SourceOwnership::Borrowed)); JS::RootedObjectVector emptyScopeChain(rq.cx); JS::RootedFunction func(rq.cx, JS::CompileFunction(rq.cx, emptyScopeChain, options, NULL, 0, NULL, src)); if (func == nullptr) { ScriptException::CatchPending(rq); return false; } JS::RootedValue rval(rq.cx); if (JS_CallFunction(rq.cx, nullptr, func, JS::HandleValueArray::empty(), &rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::LoadGlobalScript(const VfsPath& filename, const std::string& code) const { ScriptRequest rq(this); // 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(), 1); JS::SourceText src; ENSURE(src.init(rq.cx, code.c_str(), code.length(), JS::SourceOwnership::Borrowed)); if (JS::Evaluate(rq.cx, opts, src, &rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::LoadGlobalScriptFile(const VfsPath& path) const { 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; } CStr code = file.DecodeUTF8(); // assume it's UTF-8 return LoadGlobalScript(path, code); } bool ScriptInterface::Eval(const char* code) const { ScriptRequest rq(this); JS::RootedValue rval(rq.cx); JS::CompileOptions opts(rq.cx); opts.setFileAndLine("(eval)", 1); JS::SourceText src; ENSURE(src.init(rq.cx, code, strlen(code), JS::SourceOwnership::Borrowed)); if (JS::Evaluate(rq.cx, opts, src, &rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::Eval(const char* code, JS::MutableHandleValue rval) const { ScriptRequest rq(this); JS::CompileOptions opts(rq.cx); opts.setFileAndLine("(eval)", 1); JS::SourceText src; ENSURE(src.init(rq.cx, code, strlen(code), JS::SourceOwnership::Borrowed)); if (JS::Evaluate(rq.cx, opts, src, rval)) return true; ScriptException::CatchPending(rq); return false; } Index: ps/trunk/source/scriptinterface/ScriptInterface.h =================================================================== --- ps/trunk/source/scriptinterface/ScriptInterface.h (revision 27944) +++ ps/trunk/source/scriptinterface/ScriptInterface.h (revision 27945) @@ -1,301 +1,309 @@ /* Copyright (C) 2023 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_SCRIPTINTERFACE #define INCLUDED_SCRIPTINTERFACE #include "ps/Errors.h" #include "scriptinterface/ScriptConversions.h" #include "scriptinterface/ScriptExceptions.h" #include "scriptinterface/ScriptRequest.h" #include "scriptinterface/ScriptTypes.h" #include ERROR_GROUP(Scripting); ERROR_TYPE(Scripting, SetupFailed); ERROR_SUBGROUP(Scripting, LoadFile); ERROR_TYPE(Scripting_LoadFile, OpenFailed); ERROR_TYPE(Scripting_LoadFile, EvalErrors); ERROR_TYPE(Scripting, CallFunctionFailed); ERROR_TYPE(Scripting, DefineConstantFailed); ERROR_TYPE(Scripting, CreateObjectFailed); ERROR_TYPE(Scripting, TypeDoesNotExist); ERROR_SUBGROUP(Scripting, DefineType); ERROR_TYPE(Scripting_DefineType, AlreadyExists); ERROR_TYPE(Scripting_DefineType, CreationFailed); // Set the maximum number of function arguments that can be handled // (This should be as small as possible (for compiler efficiency), // but as large as necessary for all wrapped functions) #define SCRIPT_INTERFACE_MAX_ARGS 8 class ScriptInterface; struct ScriptInterface_impl; class ScriptContext; // Using a global object for the context is a workaround until Simulation, AI, etc, // use their own threads and also their own contexts. extern thread_local std::shared_ptr g_ScriptContext; namespace boost { namespace random { class rand48; } } class Path; using VfsPath = Path; /** * Abstraction around a SpiderMonkey JS::Realm. * * Thread-safety: * - May be used in non-main threads. * - Each ScriptInterface must be created, used, and destroyed, all in a single thread * (it must never be shared between threads). */ class ScriptInterface { NONCOPYABLE(ScriptInterface); friend class ScriptRequest; public: /** * Constructor. * @param nativeScopeName Name of global object that functions (via ScriptFunction::Register) will * be placed into, as a scoping mechanism; typically "Engine" * @param debugName Name of this interface for CScriptStats purposes. * @param context ScriptContext to use when initializing this interface. */ - ScriptInterface(const char* nativeScopeName, const char* debugName, const std::shared_ptr& context); + ScriptInterface(const char* nativeScopeName, const char* debugName, ScriptContext& context); + + template + ScriptInterface(const char* nativeScopeName, const char* debugName, Context&& context) : + ScriptInterface(nativeScopeName, debugName, *context) + { + static_assert(std::is_lvalue_reference_v, "`ScriptInterface` doesn't take ownership " + "of the context."); + } /** * Alternate constructor. This creates the new Realm in the same Compartment as the neighbor scriptInterface. * This means that data can be freely exchanged between these two script interfaces without cloning. * @param nativeScopeName Name of global object that functions (via ScriptFunction::Register) will * be placed into, as a scoping mechanism; typically "Engine" * @param debugName Name of this interface for CScriptStats purposes. * @param scriptInterface 'Neighbor' scriptInterface to share a compartment with. */ ScriptInterface(const char* nativeScopeName, const char* debugName, const ScriptInterface& neighbor); ~ScriptInterface(); struct CmptPrivate { friend class ScriptInterface; public: static const ScriptInterface& GetScriptInterface(JSContext* cx); static void* GetCBData(JSContext* cx); protected: ScriptInterface* pScriptInterface; // the ScriptInterface object the compartment belongs to void* pCBData; // meant to be used as the "this" object for callback functions }; void SetCallbackData(void* pCBData); /** * Convert the CmptPrivate callback data to T* */ template static T* ObjectFromCBData(const ScriptRequest& rq) { static_assert(!std::is_same_v); return static_cast(ObjectFromCBData(rq)); } /** * Variant for the function wrapper. */ template static T* ObjectFromCBData(const ScriptRequest& rq, JS::CallArgs&) { return ObjectFromCBData(rq); } /** * GetGeneralJSContext returns the context without starting a GC request and without * entering the ScriptInterface compartment. It should only be used in specific situations, * for instance when initializing a persistent rooted. * If you need the compartmented context of the ScriptInterface, you should create a * ScriptInterface::Request and use the context from that. */ JSContext* GetGeneralJSContext() const; - std::shared_ptr GetContext() const; + ScriptContext& GetContext() const; /** * Load global scripts that most script interfaces need, * located in the /globalscripts directory. VFS must be initialized. */ bool LoadGlobalScripts(); /** * Replace the default JS random number generator with a seeded, network-synced one. */ bool ReplaceNondeterministicRNG(boost::random::rand48& rng); /** * Call a constructor function, equivalent to JS "new ctor(arg)". * @param ctor An object that can be used as constructor * @param argv Constructor arguments * @param out The new object; On error an error message gets logged and out is Null (out.isNull() == true). */ void CallConstructor(JS::HandleValue ctor, JS::HandleValueArray argv, JS::MutableHandleValue out) const; JSObject* CreateCustomObject(const std::string & typeName) const; void DefineCustomObjectType(JSClass *clasp, JSNative constructor, uint minArgs, JSPropertySpec *ps, JSFunctionSpec *fs, JSPropertySpec *static_ps, JSFunctionSpec *static_fs); /** * Set the named property on the global object. * Optionally makes it {ReadOnly, DontEnum}. We do not allow to make it DontDelete, so that it can be hotloaded * by deleting it and re-creating it, which is done by setting @p replace to true. */ template bool SetGlobal(const char* name, const T& value, bool replace = false, bool constant = true, bool enumerate = true); /** * Get an object from the global scope or any lexical scope. * This can return globally accessible objects even if they are not properties * of the global object (e.g. ES6 class definitions). * @param name - Name of the property. * @param out The object or null. */ static bool GetGlobalProperty(const ScriptRequest& rq, const std::string& name, JS::MutableHandleValue out); bool SetPrototype(JS::HandleValue obj, JS::HandleValue proto); /** * Load and execute the given script in a new function scope. * @param filename Name for debugging purposes (not used to load the file) * @param code JS code to execute * @return true on successful compilation and execution; false otherwise */ bool LoadScript(const VfsPath& filename, const std::string& code) const; /** * Load and execute the given script in the global scope. * @param filename Name for debugging purposes (not used to load the file) * @param code JS code to execute * @return true on successful compilation and execution; false otherwise */ bool LoadGlobalScript(const VfsPath& filename, const std::string& code) const; /** * Load and execute the given script in the global scope. * @return true on successful compilation and execution; false otherwise */ bool LoadGlobalScriptFile(const VfsPath& path) const; /** * Evaluate some JS code in the global scope. * @return true on successful compilation and execution; false otherwise */ bool Eval(const char* code) const; bool Eval(const char* code, JS::MutableHandleValue out) const; template bool Eval(const char* code, T& out) const; /** * Calls the random number generator assigned to this ScriptInterface instance and returns the generated number. */ bool MathRandom(double& nbr) const; /** * JSNative wrapper of the above. */ static bool Math_random(JSContext* cx, uint argc, JS::Value* vp); /** * Retrieve the private data field of a JSObject that is an instance of the given JSClass. */ template static T* GetPrivate(const ScriptRequest& rq, JS::HandleObject thisobj, JSClass* jsClass) { T* value = static_cast(JS_GetInstancePrivate(rq.cx, thisobj, jsClass, nullptr)); if (value == nullptr) ScriptException::Raise(rq, "Private data of the given object is null!"); return value; } /** * Retrieve the private data field of a JS Object that is an instance of the given JSClass. * If an error occurs, GetPrivate will report it with the according stack. */ template static T* GetPrivate(const ScriptRequest& rq, JS::CallArgs& callArgs, JSClass* jsClass) { if (!callArgs.thisv().isObject()) { ScriptException::Raise(rq, "Cannot retrieve private JS class data because from a non-object value!"); return nullptr; } JS::RootedObject thisObj(rq.cx, &callArgs.thisv().toObject()); T* value = static_cast(JS_GetInstancePrivate(rq.cx, thisObj, jsClass, &callArgs)); if (value == nullptr) ScriptException::Raise(rq, "Private data of the given object is null!"); return value; } private: bool SetGlobal_(const char* name, JS::HandleValue value, bool replace, bool constant, bool enumerate); struct CustomType { JS::PersistentRootedObject m_Prototype; JSClass* m_Class; JSNative m_Constructor; }; CmptPrivate m_CmptPrivate; // Take care to keep this declaration before heap rooted members. Destructors of heap rooted // members have to be called before the custom destructor of ScriptInterface_impl. std::unique_ptr m; std::map m_CustomObjectTypes; }; // Explicitly instantiate void* as that is used for the generic template, // and we want to define it in the .cpp file. template <> void* ScriptInterface::ObjectFromCBData(const ScriptRequest& rq); template bool ScriptInterface::SetGlobal(const char* name, const T& value, bool replace, bool constant, bool enumerate) { ScriptRequest rq(this); JS::RootedValue val(rq.cx); Script::ToJSVal(rq, &val, value); return SetGlobal_(name, val, replace, constant, enumerate); } template bool ScriptInterface::Eval(const char* code, T& ret) const { ScriptRequest rq(this); JS::RootedValue rval(rq.cx); if (!Eval(code, &rval)) return false; return Script::FromJSVal(rq, rval, ret); } #endif // INCLUDED_SCRIPTINTERFACE Index: ps/trunk/source/simulation2/Simulation2.cpp =================================================================== --- ps/trunk/source/simulation2/Simulation2.cpp (revision 27944) +++ ps/trunk/source/simulation2/Simulation2.cpp (revision 27945) @@ -1,995 +1,995 @@ /* Copyright (C) 2023 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 "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 "scriptinterface/FunctionWrapper.h" #include "scriptinterface/JSON.h" #include "scriptinterface/Object.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/StructuredClone.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 #include #include class CSimulation2Impl { public: CSimulation2Impl(CUnitManager* unitManager, std::shared_ptr cx, CTerrain* terrain) : - m_SimContext(), m_ComponentManager(m_SimContext, cx), + m_SimContext(), m_ComponentManager(m_SimContext, *cx), m_EnableOOSLog(false), m_EnableSerializationTest(false), m_RejoinTestTurn(-1), m_TestingRejoin(false), m_MapSettings(cx->GetGeneralJSContext()), m_InitAttributes(cx->GetGeneralJSContext()) { 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() { 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 (NB: order matters for destruction). std::unique_ptr m_SecondaryComponentManager; std::unique_ptr m_SecondaryTerrain; std::unique_ptr m_SecondaryContext; std::unique_ptr> 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 CloneCommandsFromOtherCompartment(const ScriptInterface& newScript, const ScriptInterface& oldScript, const std::vector& commands) { std::vector newCommands; newCommands.reserve(commands.size()); ScriptRequest rqNew(newScript); for (const SimulationCommand& command : commands) { JS::RootedValue tmpCommand(rqNew.cx, Script::CloneValueFromOtherCompartment(newScript, oldScript, command.data)); Script::FreezeObject(rqNew, 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& scriptPath : pathnames) { if (loadedScripts) loadedScripts->insert(scriptPath); LOGMESSAGE("Loading simulation script '%s'", scriptPath.string8()); if (!componentManager.LoadScript(scriptPath)) ok = false; } return ok; } bool CSimulation2Impl::LoadTriggerScripts(CComponentManager& componentManager, JS::HandleValue mapSettings, std::set* loadedScripts) { bool ok = true; ScriptRequest rq(componentManager.GetScriptInterface()); if (Script::HasProperty(rq, mapSettings, "TriggerScripts")) { std::vector scriptNames; Script::GetProperty(rq, 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; ScriptRequest rq(m_ComponentManager.GetScriptInterface()); if (!Script::HasProperty(rq, m_MapSettings, "Seed") || !Script::GetProperty(rq, m_MapSettings, "Seed", seed)) LOGWARNING("CSimulation2Impl::InitRNGSeedSimulation: No seed value specified - using %d", seed); m_ComponentManager.SetRNGSeed(seed); } void CSimulation2Impl::InitRNGSeedAI() { u32 seed = 0; ScriptRequest rq(m_ComponentManager.GetScriptInterface()); if (!Script::HasProperty(rq, m_MapSettings, "AISeed") || !Script::GetProperty(rq, 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"); m_SecondaryTerrain = std::make_unique(); m_SecondaryContext = std::make_unique(); m_SecondaryContext->m_Terrain = m_SecondaryTerrain.get(); m_SecondaryComponentManager = std::make_unique(*m_SecondaryContext, scriptInterface.GetContext()); m_SecondaryComponentManager->LoadComponentTypes(); m_SecondaryLoadedScripts = std::make_unique>(); ENSURE(LoadDefaultScripts(*m_SecondaryComponentManager, m_SecondaryLoadedScripts.get())); ResetComponentState(*m_SecondaryComponentManager, false, false); ScriptRequest rq(scriptInterface); // Load the trigger scripts after we have loaded the simulation. { ScriptRequest rq2(m_SecondaryComponentManager->GetScriptInterface()); JS::RootedValue mapSettingsCloned(rq2.cx, Script::CloneValueFromOtherCompartment(m_SecondaryComponentManager->GetScriptInterface(), scriptInterface, m_MapSettings)); ENSURE(LoadTriggerScripts(*m_SecondaryComponentManager, mapSettingsCloned, m_SecondaryLoadedScripts.get())); } // Load the map into the secondary simulation LDR_BeginRegistering(); std::unique_ptr mapReader = std::make_unique(); std::string mapType; Script::GetProperty(rq, 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; Script::GetProperty(rq, m_InitAttributes, "map", mapFile); VfsPath mapfilename = VfsPath(mapFile).ChangeExtension(L".pmp"); - mapReader->LoadMap(mapfilename, *scriptInterface.GetContext(), JS::UndefinedHandleValue, + mapReader->LoadMap(mapfilename, scriptInterface.GetContext(), JS::UndefinedHandleValue, m_SecondaryTerrain.get(), NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, m_SecondaryContext.get(), 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, CloneCommandsFromOtherCompartment(m_SecondaryComponentManager->GetScriptInterface(), scriptInterface, 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.GetContext()->ShrinkingGC(); + scriptInterface.GetContext().ShrinkingGC(); else - scriptInterface.GetContext()->MaybeIncrementalGC(0.0f); + scriptInterface.GetContext().MaybeIncrementalGC(0.0f); if (m_EnableOOSLog) DumpState(); ++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->SendRequestedPaths(); { PROFILE2("Sim - Update Start"); CMessageTurnStart msgTurnStart; componentManager.BroadcastMessage(msgTurnStart); } 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->SendRequestedPaths(); } // 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->SendRequestedPaths(); } { 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(); // Compute AI immediately at turn's end. CmpPtr cmpAIManager(simContext, SYSTEM_ENTITY); if (cmpAIManager) { cmpAIManager->StartComputation(); cmpAIManager->PushCommands(); } // 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, std::shared_ptr cx, CTerrain* terrain) : m(new CSimulation2Impl(unitManager, cx, 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() { ScriptRequest rq(GetScriptInterface()); JS::RootedValue global(rq.cx, rq.globalValue()); ScriptFunction::CallVoid(rq, global, "PreInitGame"); } void CSimulation2::InitGame() { ScriptRequest rq(GetScriptInterface()); JS::RootedValue global(rq.cx, rq.globalValue()); JS::RootedValue settings(rq.cx); JS::RootedValue tmpInitAttributes(rq.cx, GetInitAttributes()); Script::GetProperty(rq, tmpInitAttributes, "settings", &settings); ScriptFunction::CallVoid(rq, 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) { Script::ParseJSON(ScriptRequest(m->m_ComponentManager.GetScriptInterface()), settings, &m->m_MapSettings); } void CSimulation2::SetMapSettings(JS::HandleValue settings) { m->m_MapSettings = settings; m->InitRNGSeedSimulation(); m->InitRNGSeedAI(); } std::string CSimulation2::GetMapSettingsString() { return Script::StringifyJSON(ScriptRequest(m->m_ComponentManager.GetScriptInterface()), &m->m_MapSettings); } void CSimulation2::GetMapSettings(JS::MutableHandleValue ret) { ret.set(m->m_MapSettings); } void CSimulation2::LoadPlayerSettings(bool newPlayers) { ScriptRequest rq(GetScriptInterface()); JS::RootedValue global(rq.cx, rq.globalValue()); ScriptFunction::CallVoid(rq, global, "LoadPlayerSettings", m->m_MapSettings, newPlayers); } void CSimulation2::LoadMapSettings() { ScriptRequest rq(GetScriptInterface()); JS::RootedValue global(rq.cx, rq.globalValue()); // Initialize here instead of in Update() ScriptFunction::CallVoid(rq, global, "LoadMapSettings", m->m_MapSettings); Script::FreezeObject(rq, 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) { stream << "sim turn: " << m->m_TurnNumber << std::endl; 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); } void CSimulation2::ActivateRejoinTest(int turn) { if (m->m_RejoinTestTurn != -1) return; LOGMESSAGERENDER("Rejoin test will activate in %i turns", turn - m->m_TurnNumber); m->m_RejoinTestTurn = turn; } 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 loadStatus = file.Load(g_VFS, p); if (loadStatus != PSRETURN_OK) { LOGERROR("GetJSONData: Failed to load file '%s': %s", p.string8(), GetErrorString(loadStatus)); 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::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(); ScriptRequest 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 (!Script::CreateObject(rq, &ais, "AIData", aiData)) return std::string(); return Script::StringifyJSON(rq, &ais); } Index: ps/trunk/source/simulation2/components/tests/test_CinemaManager.h =================================================================== --- ps/trunk/source/simulation2/components/tests/test_CinemaManager.h (revision 27944) +++ ps/trunk/source/simulation2/components/tests/test_CinemaManager.h (revision 27945) @@ -1,89 +1,89 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2023 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/ICmpCinemaManager.h" class TestCmpCinemaManager : public CxxTest::TestSuite { public: void setUp() { CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); } void test_basic() { - ComponentTestHelper test(g_ScriptContext); + ComponentTestHelper test(*g_ScriptContext); ICmpCinemaManager* cmp = test.Add(CID_CinemaManager, "", SYSTEM_ENTITY); TS_ASSERT_EQUALS(cmp->HasPath(L"test"), false); cmp->AddPath(generatePath(L"test")); TS_ASSERT_EQUALS(cmp->HasPath(L"test"), true); cmp->DeletePath(L"test"); TS_ASSERT_EQUALS(cmp->HasPath(L"test"), false); cmp->AddPath(generatePath(L"long_path", fixed::FromInt(3600))); TS_ASSERT_EQUALS(cmp->HasPath(L"long_path"), true); TS_ASSERT_EQUALS(cmp->IsEnabled(), false); cmp->AddCinemaPathToQueue(L"long_path"); cmp->Play(); size_t number_of_turns = 0; while (cmp->IsEnabled()) { CMessageUpdate msg(fixed::FromInt(36)); cmp->HandleMessage(msg, true); ++number_of_turns; } TS_ASSERT_EQUALS(number_of_turns, 100); } private: // Generates a simple cinema path with two position and two target nodes. CCinemaPath generatePath(const CStrW& name, const fixed& duration = fixed::FromInt(1)) { // Helper nodes CFixedVector3D nodeA(fixed::FromInt(1), fixed::FromInt(0), fixed::FromInt(1)); CFixedVector3D nodeB(fixed::FromInt(9), fixed::FromInt(0), fixed::FromInt(9)); CFixedVector3D shift(fixed::FromInt(3), fixed::FromInt(3), fixed::FromInt(3)); // Constructs the default cinema path data CCinemaData pathData; pathData.m_Name = name; pathData.m_Timescale = fixed::FromInt(1); pathData.m_Orientation = L"target"; pathData.m_Mode = L"ease_inout"; pathData.m_Style = L"default"; // Creates two parallel segments from the A node to the B node TNSpline positionSpline, targetSpline; positionSpline.AddNode(nodeA, CFixedVector3D(), fixed::FromInt(0)); positionSpline.AddNode(nodeB, CFixedVector3D(), duration); targetSpline.AddNode(nodeA + shift, CFixedVector3D(), fixed::FromInt(0)); targetSpline.AddNode(nodeB + shift, CFixedVector3D(), duration); return CCinemaPath(pathData, positionSpline, targetSpline); } }; Index: ps/trunk/source/simulation2/components/tests/test_CommandQueue.h =================================================================== --- ps/trunk/source/simulation2/components/tests/test_CommandQueue.h (revision 27944) +++ ps/trunk/source/simulation2/components/tests/test_CommandQueue.h (revision 27945) @@ -1,74 +1,74 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2023 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_ScriptContext); + ComponentTestHelper test(*g_ScriptContext); ScriptRequest 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_ObstructionManager.h =================================================================== --- ps/trunk/source/simulation2/components/tests/test_ObstructionManager.h (revision 27944) +++ ps/trunk/source/simulation2/components/tests/test_ObstructionManager.h (revision 27945) @@ -1,593 +1,593 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 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; ICmpObstructionManager::tag_t GetObstruction() const override { return ICmpObstructionManager::tag_t(); } bool GetObstructionSquare(ICmpObstructionManager::ObstructionSquare& out) const override { out = obstruction; return true; } bool GetPreviousObstructionSquare(ICmpObstructionManager::ObstructionSquare& UNUSED(out)) const override { return true; } entity_pos_t GetSize() const override { return entity_pos_t::Zero(); } CFixedVector2D GetStaticSize() const override { return CFixedVector2D(); } EObstructionType GetObstructionType() const override { return ICmpObstruction::STATIC; } void SetUnitClearance(const entity_pos_t& UNUSED(clearance)) override { } bool IsControlPersistent() const override { return true; } bool CheckShorePlacement() const override { return true; } EFoundationCheck CheckFoundation(const std::string& UNUSED(className)) const override { return EFoundationCheck(); } EFoundationCheck CheckFoundation(const std::string& UNUSED(className), bool UNUSED(onlyCenterPoint)) const override { return EFoundationCheck(); } std::string CheckFoundation_wrapper(const std::string& UNUSED(className), bool UNUSED(onlyCenterPoint)) const override { return std::string(); } bool CheckDuplicateFoundation() const override { return true; } std::vector GetEntitiesByFlags(ICmpObstructionManager::flags_t UNUSED(flags)) const override { return std::vector(); } std::vector GetEntitiesBlockingMovement() const override { return std::vector(); } std::vector GetEntitiesBlockingConstruction() const override { return std::vector(); } std::vector GetEntitiesDeletedUponConstruction() const override { return std::vector(); } void ResolveFoundationCollisions() const override { } void SetActive(bool UNUSED(active)) override { } void SetMovingFlag(bool UNUSED(enabled)) override { } void SetDisableBlockMovementPathfinding(bool UNUSED(movementDisabled), bool UNUSED(pathfindingDisabled), int32_t UNUSED(shape)) override { } bool GetBlockMovementFlag(bool) const override { return true; } void SetControlGroup(entity_id_t UNUSED(group)) override { } entity_id_t GetControlGroup() const override { return INVALID_ENTITY; } void SetControlGroup2(entity_id_t UNUSED(group2)) override { } entity_id_t GetControlGroup2() const override { 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_ScriptContext); + 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.0001f)); TS_ASSERT_DELTA(fixed::FromFloat(std::sqrt(80)), cmp->MaxDistanceToTarget(ent3, ent2), fixed::FromFloat(0.0001f)); 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.0001f)); TS_ASSERT_DELTA(fixed::FromFloat(std::sqrt(125) + 3), cmp->MaxDistanceToTarget(ent4, ent1), fixed::FromFloat(0.0001f)); 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.0001f)); TS_ASSERT_DELTA(fixed::FromFloat(std::sqrt(178)), cmp->MaxDistanceToTarget(ent5, ent1), fixed::FromFloat(0.0001f)); 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_Pathfinder.h =================================================================== --- ps/trunk/source/simulation2/components/tests/test_Pathfinder.h (revision 27944) +++ ps/trunk/source/simulation2/components/tests/test_Pathfinder.h (revision 27945) @@ -1,429 +1,429 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "simulation2/system/ComponentTest.h" #include "simulation2/components/ICmpObstructionManager.h" #include "simulation2/components/ICmpPathfinder.h" #include "simulation2/helpers/Grid.h" #include "graphics/MapReader.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureManager.h" #include "lib/timer.h" #include "lib/tex/tex.h" #include "ps/Loader.h" #include "ps/Pyrogenesis.h" #include "scriptinterface/ScriptContext.h" #include "simulation2/Simulation2.h" #include #include class TestCmpPathfinder : public CxxTest::TestSuite { public: void setUp() { g_VFS = CreateVfs(); g_VFS->Mount(L"", DataDir() / "mods" / "mod" / "", VFS_MOUNT_MUST_EXIST); g_VFS->Mount(L"", DataDir() / "mods" / "public" / "", VFS_MOUNT_MUST_EXIST, 1); // ignore directory-not-found errors TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir() / "_testcache" / "", 0, VFS_MAX_PRIORITY)); CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); } void test_namespace() { // Check that Pathfinding::NAVCELL_SIZE is actually an integer and that the definitions // of Pathfinding::NAVCELL_SIZE_INT and Pathfinding::NAVCELL_SIZE_LOG2 match TS_ASSERT_EQUALS(Pathfinding::NAVCELL_SIZE.ToInt_RoundToNegInfinity(), Pathfinding::NAVCELL_SIZE.ToInt_RoundToInfinity()); TS_ASSERT_EQUALS(Pathfinding::NAVCELL_SIZE.ToInt_RoundToNearest(), Pathfinding::NAVCELL_SIZE_INT); TS_ASSERT_EQUALS((Pathfinding::NAVCELL_SIZE >> 1).ToInt_RoundToZero(), Pathfinding::NAVCELL_SIZE_LOG2); } void test_pathgoal_nearest_distance() { entity_pos_t i = Pathfinding::NAVCELL_SIZE; CFixedVector2D u(i*1, i*0); CFixedVector2D v(i*0, i*1); { PathGoal goal = { PathGoal::POINT, i*8, i*6 }; TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*8 + v*4), u*8 + v*6); TS_ASSERT_EQUALS(goal.DistanceToPoint(u*8 + v*4), i*2); TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*0 + v*0), u*8 + v*6); TS_ASSERT_EQUALS(goal.DistanceToPoint(u*0 + v*0), i*10); TS_ASSERT(goal.RectContainsGoal(i*4, i*3, i*12, i*9)); TS_ASSERT(goal.RectContainsGoal(i*4, i*3, i*8, i*6)); TS_ASSERT(goal.RectContainsGoal(i*8, i*6, i*12, i*9)); TS_ASSERT(!goal.RectContainsGoal(i*4, i*3, i*7, i*5)); TS_ASSERT(!goal.RectContainsGoal(i*9, i*7, i*13, i*15)); } { PathGoal goal = { PathGoal::CIRCLE, i*8, i*6, i*5 }; TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*8 + v*4), u*8 + v*4); TS_ASSERT_EQUALS(goal.DistanceToPoint(u*8 + v*4), i*0); TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*0 + v*0), u*4 + v*3); TS_ASSERT_EQUALS(goal.DistanceToPoint(u*0 + v*0), i*5); TS_ASSERT(goal.RectContainsGoal(i*7, i*5, i*9, i*7)); // fully inside TS_ASSERT(goal.RectContainsGoal(i*3, i*1, i*13, i*11)); // fully outside TS_ASSERT(goal.RectContainsGoal(i*4, i*3, i*8, i*6)); // partially inside TS_ASSERT(goal.RectContainsGoal(i*4, i*0, i*12, i*1)); // touching the edge } { PathGoal goal = { PathGoal::INVERTED_CIRCLE, i*8, i*6, i*5 }; TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*8 + v*4), u*8 + v*1); TS_ASSERT_EQUALS(goal.DistanceToPoint(u*8 + v*4), i*3); TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*0 + v*0), u*0 + v*0); TS_ASSERT_EQUALS(goal.DistanceToPoint(u*0 + v*0), i*0); TS_ASSERT(!goal.RectContainsGoal(i*7, i*5, i*9, i*7)); // fully inside TS_ASSERT(goal.RectContainsGoal(i*3, i*1, i*13, i*11)); // fully outside TS_ASSERT(goal.RectContainsGoal(i*4, i*3, i*8, i*6)); // partially inside TS_ASSERT(goal.RectContainsGoal(i*4, i*0, i*12, i*1)); // touching the edge } { PathGoal goal = { PathGoal::SQUARE, i*8, i*6, i*4, i*3, u, v }; TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*8 + v*4), u*8 + v*4); TS_ASSERT_EQUALS(goal.DistanceToPoint(u*8 + v*4), i*0); TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*0 + v*0), u*4 + v*3); TS_ASSERT_EQUALS(goal.DistanceToPoint(u*0 + v*0), i*5); TS_ASSERT(goal.RectContainsGoal(i*7, i*5, i*9, i*7)); // fully inside TS_ASSERT(goal.RectContainsGoal(i*3, i*1, i*13, i*11)); // fully outside TS_ASSERT(goal.RectContainsGoal(i*4, i*3, i*8, i*6)); // partially inside TS_ASSERT(goal.RectContainsGoal(i*4, i*2, i*12, i*3)); // touching the edge TS_ASSERT(goal.RectContainsGoal(i*3, i*0, i*4, i*10)); // touching the edge } { PathGoal goal = { PathGoal::INVERTED_SQUARE, i*8, i*6, i*4, i*3, u, v }; TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*8 + v*4), u*8 + v*3); TS_ASSERT_EQUALS(goal.DistanceToPoint(u*8 + v*4), i*1); TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*0 + v*0), u*0 + v*0); TS_ASSERT_EQUALS(goal.DistanceToPoint(u*0 + v*0), i*0); TS_ASSERT(!goal.RectContainsGoal(i*7, i*5, i*9, i*7)); // fully inside TS_ASSERT(goal.RectContainsGoal(i*3, i*1, i*13, i*11)); // fully outside TS_ASSERT(!goal.RectContainsGoal(i*4, i*3, i*8, i*6)); // inside, touching (should fail) TS_ASSERT(goal.RectContainsGoal(i*4, i*2, i*12, i*3)); // touching the edge TS_ASSERT(goal.RectContainsGoal(i*3, i*0, i*4, i*10)); // touching the edge } } void test_performance_DISABLED() { CTerrain terrain; CSimulation2 sim2(NULL, g_ScriptContext, &terrain); sim2.LoadDefaultScripts(); sim2.ResetState(); std::unique_ptr mapReader = std::make_unique(); LDR_BeginRegistering(); mapReader->LoadMap(L"maps/skirmishes/Median Oasis (2).pmp", - *sim2.GetScriptInterface().GetContext(), JS::UndefinedHandleValue, + sim2.GetScriptInterface().GetContext(), JS::UndefinedHandleValue, &terrain, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &sim2, &sim2.GetSimContext(), -1, false); LDR_EndRegistering(); TS_ASSERT_OK(LDR_NonprogressiveLoad()); sim2.PreInitGame(); sim2.InitGame(); sim2.Update(0); CmpPtr cmp(sim2, SYSTEM_ENTITY); #if 0 entity_pos_t x0 = entity_pos_t::FromInt(10); entity_pos_t z0 = entity_pos_t::FromInt(495); entity_pos_t x1 = entity_pos_t::FromInt(500); entity_pos_t z1 = entity_pos_t::FromInt(495); ICmpPathfinder::Goal goal = { ICmpPathfinder::Goal::POINT, x1, z1 }; WaypointPath path; cmp->ComputePath(x0, z0, goal, cmp->GetPassabilityClass("default"), path); for (size_t i = 0; i < path.m_Waypoints.size(); ++i) printf("%d: %f %f\n", (int)i, path.m_Waypoints[i].x.ToDouble(), path.m_Waypoints[i].z.ToDouble()); #endif double t = timer_Time(); std::mt19937 engine(42); std::uniform_int_distribution distribution511(0, 511); std::uniform_int_distribution distribution63(0, 63); for (size_t j = 0; j < 1024*2; ++j) { entity_pos_t x0 = entity_pos_t::FromInt(distribution511(engine)); entity_pos_t z0 = entity_pos_t::FromInt(distribution511(engine)); entity_pos_t x1 = x0 + entity_pos_t::FromInt(distribution63(engine)); entity_pos_t z1 = z0 + entity_pos_t::FromInt(distribution63(engine)); PathGoal goal = { PathGoal::POINT, x1, z1 }; WaypointPath path; cmp->ComputePathImmediate(x0, z0, goal, cmp->GetPassabilityClass("default"), path); } t = timer_Time() - t; printf("[%f]", t); } void test_performance_short_DISABLED() { CTerrain terrain; terrain.Initialize(5, NULL); CSimulation2 sim2(NULL, g_ScriptContext, &terrain); sim2.LoadDefaultScripts(); sim2.ResetState(); const entity_pos_t range = entity_pos_t::FromInt(48); CmpPtr cmpObstructionMan(sim2, SYSTEM_ENTITY); CmpPtr cmpPathfinder(sim2, SYSTEM_ENTITY); std::mt19937 engine(42); std::uniform_real_distribution distribution01(0.0f, std::nextafter(1.0f, 2.0f)); for (size_t i = 0; i < 200; ++i) { fixed x = fixed::FromFloat(1.5f*range.ToFloat() * distribution01(engine)); fixed z = fixed::FromFloat(1.5f*range.ToFloat() * distribution01(engine)); // printf("# %f %f\n", x.ToFloat(), z.ToFloat()); cmpObstructionMan->AddUnitShape(INVALID_ENTITY, x, z, fixed::FromInt(2), 0, INVALID_ENTITY); } PathGoal goal = { PathGoal::POINT, range, range }; WaypointPath path = cmpPathfinder->ComputeShortPathImmediate(ShortPathRequest{ 0, range/3, range/3, fixed::FromInt(2), range, goal, 0, false, 0, 0 }); for (size_t i = 0; i < path.m_Waypoints.size(); ++i) printf("# %d: %f %f\n", (int)i, path.m_Waypoints[i].x.ToFloat(), path.m_Waypoints[i].z.ToFloat()); } template void DumpGrid(std::ostream& stream, const Grid& grid, int mask) { for (u16 j = 0; j < grid.m_H; ++j) { for (u16 i = 0; i < grid.m_W; ) { if (!(grid.get(i, j) & mask)) { i++; continue; } u16 i0 = i; for (i = i0+1; ; ++i) { if (i >= grid.m_W || !(grid.get(i, j) & mask)) { stream << " \n"; break; } } } } } void test_perf2_DISABLED() { CTerrain terrain; CSimulation2 sim2(NULL, g_ScriptContext, &terrain); sim2.LoadDefaultScripts(); sim2.ResetState(); std::unique_ptr mapReader = std::make_unique(); LDR_BeginRegistering(); mapReader->LoadMap(L"maps/scenarios/Peloponnese.pmp", - *sim2.GetScriptInterface().GetContext(), JS::UndefinedHandleValue, + sim2.GetScriptInterface().GetContext(), JS::UndefinedHandleValue, &terrain, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &sim2, &sim2.GetSimContext(), -1, false); LDR_EndRegistering(); TS_ASSERT_OK(LDR_NonprogressiveLoad()); sim2.PreInitGame(); sim2.InitGame(); sim2.Update(0); std::ofstream stream(OsString("perf2.html").c_str(), std::ofstream::out | std::ofstream::trunc); CmpPtr cmpObstructionManager(sim2, SYSTEM_ENTITY); CmpPtr cmpPathfinder(sim2, SYSTEM_ENTITY); pass_class_t obstructionsMask = cmpPathfinder->GetPassabilityClass("default"); const Grid& obstructions = cmpPathfinder->GetPassabilityGrid(); int scale = 1; stream << "\n"; stream << "\n"; stream << "\n"; stream << "\n"; stream << " \n"; DumpGrid(stream, obstructions, obstructionsMask); stream << " \n"; DumpPath(stream, 128*4, 256*4, 128*4, 384*4, cmpPathfinder); // RepeatPath(500, 128*4, 256*4, 128*4, 384*4, cmpPathfinder); // // DumpPath(stream, 128*4, 204*4, 192*4, 204*4, cmpPathfinder); // // DumpPath(stream, 128*4, 230*4, 32*4, 230*4, cmpPathfinder); stream << "\n"; stream << "\n"; } void test_perf3_DISABLED() { CTerrain terrain; CSimulation2 sim2(NULL, g_ScriptContext, &terrain); sim2.LoadDefaultScripts(); sim2.ResetState(); std::unique_ptr mapReader = std::make_unique(); LDR_BeginRegistering(); mapReader->LoadMap(L"maps/scenarios/Peloponnese.pmp", - *sim2.GetScriptInterface().GetContext(), JS::UndefinedHandleValue, + sim2.GetScriptInterface().GetContext(), JS::UndefinedHandleValue, &terrain, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &sim2, &sim2.GetSimContext(), -1, false); LDR_EndRegistering(); TS_ASSERT_OK(LDR_NonprogressiveLoad()); sim2.PreInitGame(); sim2.InitGame(); sim2.Update(0); std::ofstream stream(OsString("perf3.html").c_str(), std::ofstream::out | std::ofstream::trunc); CmpPtr cmpObstructionManager(sim2, SYSTEM_ENTITY); CmpPtr cmpPathfinder(sim2, SYSTEM_ENTITY); pass_class_t obstructionsMask = cmpPathfinder->GetPassabilityClass("default"); const Grid& obstructions = cmpPathfinder->GetPassabilityGrid(); int scale = 31; stream << "\n"; stream << "\n"; stream << "\n"; stream << "\n"; stream << "\n"; stream << "\n"; stream << "\n"; stream << " \n"; DumpGrid(stream, obstructions, obstructionsMask); stream << " \n"; for (int j = 160; j < 190; ++j) for (int i = 220; i < 290; ++i) DumpPath(stream, 230, 178, i, j, cmpPathfinder); stream << "\n"; stream << "\n"; stream << "\n"; } void DumpPath(std::ostream& stream, int i0, int j0, int i1, int j1, CmpPtr& cmpPathfinder) { entity_pos_t x0 = entity_pos_t::FromInt(i0); entity_pos_t z0 = entity_pos_t::FromInt(j0); entity_pos_t x1 = entity_pos_t::FromInt(i1); entity_pos_t z1 = entity_pos_t::FromInt(j1); PathGoal goal = { PathGoal::POINT, x1, z1 }; WaypointPath path; cmpPathfinder->ComputePathImmediate(x0, z0, goal, cmpPathfinder->GetPassabilityClass("default"), path); u32 debugSteps; double debugTime; Grid debugGrid; cmpPathfinder->GetDebugData(debugSteps, debugTime, debugGrid); // stream << " \n"; stream << " \n"; // stream << " \n"; // DumpGrid(stream, debugGrid, 1); // stream << " \n"; // stream << " \n"; // DumpGrid(stream, debugGrid, 2); // stream << " \n"; // stream << " \n"; // DumpGrid(stream, debugGrid, 3); // stream << " \n"; stream << " \n"; stream << " \n"; } void RepeatPath(int n, int i0, int j0, int i1, int j1, CmpPtr& cmpPathfinder) { entity_pos_t x0 = entity_pos_t::FromInt(i0); entity_pos_t z0 = entity_pos_t::FromInt(j0); entity_pos_t x1 = entity_pos_t::FromInt(i1); entity_pos_t z1 = entity_pos_t::FromInt(j1); PathGoal goal = { PathGoal::POINT, x1, z1 }; double t = timer_Time(); for (int i = 0; i < n; ++i) { WaypointPath path; cmpPathfinder->ComputePathImmediate(x0, z0, goal, cmpPathfinder->GetPassabilityClass("default"), path); } t = timer_Time() - t; debug_printf("### RepeatPath %fms each (%fs total)\n", 1000*t / n, t); } }; Index: ps/trunk/source/simulation2/components/tests/test_Position.h =================================================================== --- ps/trunk/source/simulation2/components/tests/test_Position.h (revision 27944) +++ ps/trunk/source/simulation2/components/tests/test_Position.h (revision 27945) @@ -1,233 +1,233 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 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 "maths/Matrix3D.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpWaterManager.h" class MockWater : public ICmpWaterManager { public: DEFAULT_MOCK_COMPONENT() entity_pos_t GetWaterLevel(entity_pos_t UNUSED(x), entity_pos_t UNUSED(z)) const override { return entity_pos_t::FromInt(100); } float GetExactWaterLevel(float UNUSED(x), float UNUSED(z)) const override { return 100.f; } void RecomputeWaterData() override { } void SetWaterLevel(entity_pos_t UNUSED(h)) override { } }; 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_ScriptContext); + 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_ScriptContext); + 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_ScriptContext); + 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/components/tests/test_RangeManager.h =================================================================== --- ps/trunk/source/simulation2/components/tests/test_RangeManager.h (revision 27944) +++ ps/trunk/source/simulation2/components/tests/test_RangeManager.h (revision 27945) @@ -1,313 +1,313 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 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 "maths/Matrix3D.h" #include "simulation2/system/ComponentTest.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/components/ICmpObstruction.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpVision.h" #include #include class MockVisionRgm : public ICmpVision { public: DEFAULT_MOCK_COMPONENT() entity_pos_t GetRange() const override { return entity_pos_t::FromInt(66); } bool GetRevealShore() const override { return false; } }; class MockPositionRgm : public ICmpPosition { public: DEFAULT_MOCK_COMPONENT() void SetTurretParent(entity_id_t UNUSED(id), const CFixedVector3D& UNUSED(pos)) override {} entity_id_t GetTurretParent() const override {return INVALID_ENTITY;} void UpdateTurretPosition() override {} std::set* GetTurrets() override { return nullptr; } bool IsInWorld() const override { return true; } void MoveOutOfWorld() override { } void MoveTo(entity_pos_t UNUSED(x), entity_pos_t UNUSED(z)) override { } void MoveAndTurnTo(entity_pos_t UNUSED(x), entity_pos_t UNUSED(z), entity_angle_t UNUSED(a)) override { } void JumpTo(entity_pos_t UNUSED(x), entity_pos_t UNUSED(z)) override { } void SetHeightOffset(entity_pos_t UNUSED(dy)) override { } entity_pos_t GetHeightOffset() const override { return entity_pos_t::Zero(); } void SetHeightFixed(entity_pos_t UNUSED(y)) override { } entity_pos_t GetHeightFixed() const override { return entity_pos_t::Zero(); } entity_pos_t GetHeightAtFixed(entity_pos_t, entity_pos_t) const override { return entity_pos_t::Zero(); } bool IsHeightRelative() const override { return true; } void SetHeightRelative(bool UNUSED(relative)) override { } bool CanFloat() const override { return false; } void SetFloating(bool UNUSED(flag)) override { } void SetActorFloating(bool UNUSED(flag)) override { } void SetConstructionProgress(fixed UNUSED(progress)) override { } CFixedVector3D GetPosition() const override { return m_Pos; } CFixedVector2D GetPosition2D() const override { return CFixedVector2D(m_Pos.X, m_Pos.Z); } CFixedVector3D GetPreviousPosition() const override { return CFixedVector3D(); } CFixedVector2D GetPreviousPosition2D() const override { return CFixedVector2D(); } fixed GetTurnRate() const override { return fixed::Zero(); } void TurnTo(entity_angle_t UNUSED(y)) override { } void SetYRotation(entity_angle_t UNUSED(y)) override { } void SetXZRotation(entity_angle_t UNUSED(x), entity_angle_t UNUSED(z)) override { } CFixedVector3D GetRotation() const override { return CFixedVector3D(); } fixed GetDistanceTravelled() const override { return fixed::Zero(); } void GetInterpolatedPosition2D(float UNUSED(frameOffset), float& x, float& z, float& rotY) const override { x = z = rotY = 0; } CMatrix3D GetInterpolatedTransform(float UNUSED(frameOffset)) const override { return CMatrix3D(); } CFixedVector3D m_Pos; }; class MockObstructionRgm : public ICmpObstruction { public: DEFAULT_MOCK_COMPONENT(); MockObstructionRgm(entity_pos_t s) : m_Size(s) {}; ICmpObstructionManager::tag_t GetObstruction() const override { return {}; }; bool GetObstructionSquare(ICmpObstructionManager::ObstructionSquare&) const override { return false; }; bool GetPreviousObstructionSquare(ICmpObstructionManager::ObstructionSquare&) const override { return false; }; entity_pos_t GetSize() const override { return m_Size; }; CFixedVector2D GetStaticSize() const override { return {}; }; EObstructionType GetObstructionType() const override { return {}; }; void SetUnitClearance(const entity_pos_t&) override {}; bool IsControlPersistent() const override { return {}; }; bool CheckShorePlacement() const override { return {}; }; EFoundationCheck CheckFoundation(const std::string&) const override { return {}; }; EFoundationCheck CheckFoundation(const std::string& , bool) const override { return {}; }; std::string CheckFoundation_wrapper(const std::string&, bool) const override { return {}; }; bool CheckDuplicateFoundation() const override { return {}; }; std::vector GetEntitiesByFlags(ICmpObstructionManager::flags_t) const override { return {}; }; std::vector GetEntitiesBlockingMovement() const override { return {}; }; std::vector GetEntitiesBlockingConstruction() const override { return {}; }; std::vector GetEntitiesDeletedUponConstruction() const override { return {}; }; void ResolveFoundationCollisions() const override {}; void SetActive(bool) override {}; void SetMovingFlag(bool) override {}; void SetDisableBlockMovementPathfinding(bool, bool, int32_t) override {}; bool GetBlockMovementFlag(bool) const override { return {}; }; void SetControlGroup(entity_id_t) override {}; entity_id_t GetControlGroup() const override { return {}; }; void SetControlGroup2(entity_id_t) override {}; entity_id_t GetControlGroup2() const override { return {}; }; private: entity_pos_t m_Size; }; 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_ScriptContext); + ComponentTestHelper test(*g_ScriptContext); ICmpRangeManager* cmp = test.Add(CID_RangeManager, "", SYSTEM_ENTITY); MockVisionRgm vision; test.AddMock(100, IID_Vision, vision); MockPositionRgm 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)); 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; } } } void test_queries() { - ComponentTestHelper test(g_ScriptContext); + ComponentTestHelper test(*g_ScriptContext); ICmpRangeManager* cmp = test.Add(CID_RangeManager, "", SYSTEM_ENTITY); MockVisionRgm vision, vision2; MockPositionRgm position, position2; MockObstructionRgm obs(fixed::FromInt(2)), obs2(fixed::Zero()); test.AddMock(100, IID_Vision, vision); test.AddMock(100, IID_Position, position); test.AddMock(100, IID_Obstruction, obs); test.AddMock(101, IID_Vision, vision2); test.AddMock(101, IID_Position, position2); test.AddMock(101, IID_Obstruction, obs2); cmp->SetBounds(entity_pos_t::FromInt(0), entity_pos_t::FromInt(0), entity_pos_t::FromInt(512), entity_pos_t::FromInt(512)); cmp->Verify(); { CMessageCreate msg(100); cmp->HandleMessage(msg, false); } { CMessageCreate msg(101); cmp->HandleMessage(msg, false); } { CMessageOwnershipChanged msg(100, -1, 1); cmp->HandleMessage(msg, false); } { CMessageOwnershipChanged msg(101, -1, 1); cmp->HandleMessage(msg, false); } auto move = [&cmp](entity_id_t ent, MockPositionRgm& pos, fixed x, fixed z) { pos.m_Pos = CFixedVector3D(x, fixed::Zero(), z); { CMessagePositionChanged msg(ent, true, x, z, entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } }; move(100, position, fixed::FromInt(10), fixed::FromInt(10)); move(101, position2, fixed::FromInt(10), fixed::FromInt(20)); std::vector nearby = cmp->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {1}, 0, true); TS_ASSERT_EQUALS(nearby, std::vector{}); nearby = cmp->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {1}, 0, true); TS_ASSERT_EQUALS(nearby, std::vector{101}); move(101, position2, fixed::FromInt(10), fixed::FromInt(10)); nearby = cmp->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {1}, 0, true); TS_ASSERT_EQUALS(nearby, std::vector{101}); nearby = cmp->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {1}, 0, true); TS_ASSERT_EQUALS(nearby, std::vector{}); move(101, position2, fixed::FromInt(10), fixed::FromInt(13)); nearby = cmp->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {1}, 0, true); TS_ASSERT_EQUALS(nearby, std::vector{101}); nearby = cmp->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {1}, 0, true); TS_ASSERT_EQUALS(nearby, std::vector{}); move(101, position2, fixed::FromInt(10), fixed::FromInt(15)); // In range thanks to self obstruction size. nearby = cmp->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {1}, 0, true); TS_ASSERT_EQUALS(nearby, std::vector{101}); // In range thanks to target obstruction size. nearby = cmp->ExecuteQuery(101, fixed::FromInt(0), fixed::FromInt(4), {1}, 0, true); TS_ASSERT_EQUALS(nearby, std::vector{100}); // Trickier: min-range is closest-to-closest, but rotation may change the real distance. nearby = cmp->ExecuteQuery(100, fixed::FromInt(2), fixed::FromInt(50), {1}, 0, true); TS_ASSERT_EQUALS(nearby, std::vector{101}); nearby = cmp->ExecuteQuery(100, fixed::FromInt(5), fixed::FromInt(50), {1}, 0, true); TS_ASSERT_EQUALS(nearby, std::vector{101}); nearby = cmp->ExecuteQuery(100, fixed::FromInt(6), fixed::FromInt(50), {1}, 0, true); TS_ASSERT_EQUALS(nearby, std::vector{}); nearby = cmp->ExecuteQuery(101, fixed::FromInt(5), fixed::FromInt(50), {1}, 0, true); TS_ASSERT_EQUALS(nearby, std::vector{100}); nearby = cmp->ExecuteQuery(101, fixed::FromInt(6), fixed::FromInt(50), {1}, 0, true); TS_ASSERT_EQUALS(nearby, std::vector{}); } void test_IsInTargetParabolicRange() { - ComponentTestHelper test(g_ScriptContext); + ComponentTestHelper test(*g_ScriptContext); ICmpRangeManager* cmp = test.Add(CID_RangeManager, "", SYSTEM_ENTITY); const entity_id_t source = 200; const entity_id_t target = 201; entity_pos_t range = fixed::FromInt(-3); entity_pos_t yOrigin = fixed::FromInt(-20); // Invalid range. TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), range); // No source ICmpPosition. range = fixed::FromInt(10); TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), NEVER_IN_RANGE); // No target ICmpPosition. MockPositionRgm cmpSourcePosition; test.AddMock(source, IID_Position, cmpSourcePosition); TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), NEVER_IN_RANGE); // Too much height difference. MockPositionRgm cmpTargetPosition; test.AddMock(target, IID_Position, cmpTargetPosition); TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), NEVER_IN_RANGE); // If no offset we get the range. range = fixed::FromInt(20); yOrigin = fixed::Zero(); TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), range); TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, fixed::Zero(), yOrigin), fixed::Zero()); // Normal case. yOrigin = fixed::FromInt(5); range = fixed::FromInt(10); TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), fixed::FromFloat(14.142136f)); // Big range. range = fixed::FromInt(260); TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), fixed::FromFloat(264.952820f)); } }; Index: ps/trunk/source/simulation2/components/tests/test_TerritoryManager.h =================================================================== --- ps/trunk/source/simulation2/components/tests/test_TerritoryManager.h (revision 27944) +++ ps/trunk/source/simulation2/components/tests/test_TerritoryManager.h (revision 27945) @@ -1,453 +1,453 @@ /* Copyright (C) 2023 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 "maths/Matrix3D.h" #include "ps/CStr.h" #include "graphics/Terrain.h" #include "graphics/TerritoryBoundary.h" #include "simulation2/helpers/Grid.h" #include "simulation2/components/ICmpTerritoryManager.h" #include "simulation2/components/ICmpPlayerManager.h" #include "simulation2/components/ICmpTerritoryInfluence.h" #include "simulation2/components/ICmpOwnership.h" class MockPathfinderTerrMan : public ICmpPathfinder { public: DEFAULT_MOCK_COMPONENT() // Test data Grid m_PassabilityGrid; virtual pass_class_t GetPassabilityClass(const std::string&) const override { return 0; } virtual const Grid& GetPassabilityGrid() override { return m_PassabilityGrid; } // Irrelevant part of the mock. virtual void GetPassabilityClasses(std::map&) const override {} virtual void GetPassabilityClasses(std::map&, std::map&) const override {} virtual entity_pos_t GetClearance(pass_class_t) const override { return entity_pos_t::FromInt(1); } virtual entity_pos_t GetMaximumClearance() const override { return entity_pos_t::FromInt(1); } virtual const GridUpdateInformation& GetAIPathfinderDirtinessInformation() const override { static GridUpdateInformation gridInfo; return gridInfo; } virtual void FlushAIPathfinderDirtinessInformation() override {} virtual Grid ComputeShoreGrid(bool = false) override { return Grid {}; } virtual u32 ComputePathAsync(entity_pos_t, entity_pos_t, const PathGoal&, pass_class_t, entity_id_t) override { return 1; } virtual void ComputePathImmediate(entity_pos_t, entity_pos_t, const PathGoal&, pass_class_t, WaypointPath&) const override {} virtual u32 ComputeShortPathAsync(entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t, const PathGoal&, pass_class_t, bool, entity_id_t, entity_id_t) override { return 1; } virtual WaypointPath ComputeShortPathImmediate(const ShortPathRequest&) const override { return WaypointPath(); } virtual void SetDebugPath(entity_pos_t, entity_pos_t, const PathGoal&, pass_class_t) override {} virtual bool IsGoalReachable(entity_pos_t, entity_pos_t, const PathGoal&, pass_class_t) override { return false; } virtual bool CheckMovement(const IObstructionTestFilter&, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t, pass_class_t) const override { return false; } virtual ICmpObstruction::EFoundationCheck CheckUnitPlacement(const IObstructionTestFilter&, entity_pos_t, entity_pos_t, entity_pos_t, pass_class_t, bool = false) const override { return ICmpObstruction::FOUNDATION_CHECK_SUCCESS; } virtual ICmpObstruction::EFoundationCheck CheckBuildingPlacement(const IObstructionTestFilter&, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t, entity_id_t, pass_class_t) const override { return ICmpObstruction::FOUNDATION_CHECK_SUCCESS; } virtual ICmpObstruction::EFoundationCheck CheckBuildingPlacement(const IObstructionTestFilter&, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t, entity_id_t, pass_class_t, bool) const override { return ICmpObstruction::FOUNDATION_CHECK_SUCCESS; } virtual void SetDebugOverlay(bool) override {} virtual void SetHierDebugOverlay(bool) override {} virtual void SendRequestedPaths() override {} virtual void StartProcessingMoves(bool) override {} virtual void UpdateGrid() override {} virtual void GetDebugData(u32&, double&, Grid&) const override {} virtual void SetAtlasOverlay(bool, pass_class_t = 0) override {} }; class MockPlayerMgrTerrMan : public ICmpPlayerManager { public: DEFAULT_MOCK_COMPONENT() int32_t GetNumPlayers() override { return 2; } entity_id_t GetPlayerByID(int32_t id) override { return id + 1; } }; class MockTerrInfTerrMan : public ICmpTerritoryInfluence { public: DEFAULT_MOCK_COMPONENT() bool IsRoot() const override { return true; }; u16 GetWeight() const override { return 10; }; u32 GetRadius() const override { return m_Radius; }; u32 m_Radius = 0; }; class MockOwnershipTerrMan : public ICmpOwnership { public: DEFAULT_MOCK_COMPONENT() player_id_t GetOwner() const override { return 1; }; void SetOwner(player_id_t) override {}; void SetOwnerQuiet(player_id_t) override {}; }; class MockPositionTerrMan : public ICmpPosition { public: DEFAULT_MOCK_COMPONENT() void SetTurretParent(entity_id_t, const CFixedVector3D&) override {} entity_id_t GetTurretParent() const override { return INVALID_ENTITY; } void UpdateTurretPosition() override {} std::set* GetTurrets() override { return nullptr; } bool IsInWorld() const override { return true; } void MoveOutOfWorld() override {} void MoveTo(entity_pos_t, entity_pos_t) override {} void MoveAndTurnTo(entity_pos_t, entity_pos_t, entity_angle_t) override {} void JumpTo(entity_pos_t, entity_pos_t) override {} void SetHeightOffset(entity_pos_t) override {} entity_pos_t GetHeightOffset() const override { return entity_pos_t::Zero(); } void SetHeightFixed(entity_pos_t) override {} entity_pos_t GetHeightFixed() const override { return entity_pos_t::Zero(); } entity_pos_t GetHeightAtFixed(entity_pos_t, entity_pos_t) const override { return entity_pos_t::Zero(); } bool IsHeightRelative() const override { return true; } void SetHeightRelative(bool) override {} bool CanFloat() const override { return false; } void SetFloating(bool) override {} void SetActorFloating(bool) override {} void SetConstructionProgress(fixed) override {} CFixedVector3D GetPosition() const override { return m_Pos; } CFixedVector2D GetPosition2D() const override { return CFixedVector2D(m_Pos.X, m_Pos.Z); } CFixedVector3D GetPreviousPosition() const override { return CFixedVector3D(); } CFixedVector2D GetPreviousPosition2D() const override { return CFixedVector2D(); } fixed GetTurnRate() const override { return fixed::Zero(); } void TurnTo(entity_angle_t) override {} void SetYRotation(entity_angle_t) override {} void SetXZRotation(entity_angle_t, entity_angle_t) override {} CFixedVector3D GetRotation() const override { return CFixedVector3D(); } fixed GetDistanceTravelled() const override { return fixed::Zero(); } void GetInterpolatedPosition2D(float, float&, float&, float&) const override {} CMatrix3D GetInterpolatedTransform(float) const override { return CMatrix3D(); } CFixedVector3D m_Pos; }; class TestCmpTerritoryManager : public CxxTest::TestSuite { public: void setUp() { CxxTest::setAbortTestOnFail(true); 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" / "", 0, VFS_MAX_PRIORITY)); CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); } // Regression test for D5181 / fix for rP27673 issue void test_calculate_territories_uninitialised() { - ComponentTestHelper test(g_ScriptContext); + ComponentTestHelper test(*g_ScriptContext); ICmpTerritoryManager* cmp = test.Add(CID_TerritoryManager, "", SYSTEM_ENTITY); MockPathfinderTerrMan pathfinder; test.AddMock(SYSTEM_ENTITY, IID_Pathfinder, pathfinder); MockPlayerMgrTerrMan playerMan; test.AddMock(SYSTEM_ENTITY, IID_PlayerManager, playerMan); pathfinder.m_PassabilityGrid.resize(ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE * 5, ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE * 5); MockTerrInfTerrMan terrInf; test.AddMock(5, IID_TerritoryInfluence, terrInf); MockOwnershipTerrMan ownership; test.AddMock(5, IID_Ownership, ownership); MockPositionTerrMan position; test.AddMock(5, IID_Position, position); position.m_Pos = CFixedVector3D( entity_pos_t::FromInt(ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE * 5 + ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE / 2), entity_pos_t::FromInt(1), entity_pos_t::FromInt(ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE * 5 + ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE / 2) ); terrInf.m_Radius = 1; TS_ASSERT_EQUALS(cmp->GetTerritoryPercentage(0), 0); TS_ASSERT_EQUALS(cmp->GetTerritoryPercentage(1), 4); // 5*5 = 25 -> 1 tile out of 25 is 4% TS_ASSERT_EQUALS(cmp->GetTerritoryPercentage(2), 0); terrInf.m_Radius = ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE * 10; test.HandleMessage(cmp, CMessageTerrainChanged(0, 0, 0, 0), true); TS_ASSERT_EQUALS(cmp->GetTerritoryPercentage(0), 0); TS_ASSERT_EQUALS(cmp->GetTerritoryPercentage(1), 100); TS_ASSERT_EQUALS(cmp->GetTerritoryPercentage(2), 0); } void test_boundaries() { Grid grid = GetGrid("--------" "777777--" "777777--" "777777--" "--------", 8, 5); std::vector boundaries = CTerritoryBoundaryCalculator::ComputeBoundaries(&grid); TS_ASSERT_EQUALS(1U, boundaries.size()); TS_ASSERT_EQUALS(18U, boundaries[0].points.size()); // 2x6 + 2x3 TS_ASSERT_EQUALS((player_id_t)7, boundaries[0].owner); TS_ASSERT_EQUALS(false, boundaries[0].blinking); // high bits aren't set by GetGrid // assumes NAVCELLS_PER_TERRITORY_TILE is 8; dealt with in TestBoundaryPointsEqual int expectedPoints[][2] = {{ 4, 8}, {12, 8}, {20, 8}, {28, 8}, {36, 8}, {44, 8}, {48,12}, {48,20}, {48,28}, {44,32}, {36,32}, {28,32}, {20,32}, {12,32}, { 4,32}, { 0,28}, { 0,20}, { 0,12}}; TestBoundaryPointsEqual(boundaries[0].points, expectedPoints); } void test_nested_boundaries1() { // test case from ticket #918; contains single-tile territories with double borders Grid grid1 = GetGrid("--------" "-111111-" "-1-1213-" "-111111-" "--------", 8, 5); std::vector boundaries = CTerritoryBoundaryCalculator::ComputeBoundaries(&grid1); size_t expectedNumBoundaries = 5; TS_ASSERT_EQUALS(expectedNumBoundaries, boundaries.size()); STerritoryBoundary* onesOuter = NULL; STerritoryBoundary* onesInner0 = NULL; // inner border around the neutral tile STerritoryBoundary* onesInner2 = NULL; // inner border around the '2' tile STerritoryBoundary* twosOuter = NULL; STerritoryBoundary* threesOuter = NULL; // expected number of points (!) in the inner boundaries for terrain 1 (there are two with the same size) size_t onesInnerNumExpectedPoints = 4; for (size_t i=0; ipoints.size(), 20U); TS_ASSERT_EQUALS(onesInner0->points.size(), 4U); TS_ASSERT_EQUALS(onesInner2->points.size(), 4U); TS_ASSERT_EQUALS(twosOuter->points.size(), 4U); TS_ASSERT_EQUALS(threesOuter->points.size(), 4U); int onesOuterExpectedPoints[][2] = {{12, 8}, {20, 8}, {28, 8}, {36, 8}, {44, 8}, {52, 8}, {56,12}, {52,16}, {48,20}, {52,24}, {56,28}, {52,32}, {44,32}, {36,32}, {28,32}, {20,32}, {12,32}, { 8,28}, { 8,20}, { 8,12}}; int onesInner0ExpectedPoints[][2] = {{20,24}, {24,20}, {20,16}, {16,20}}; int onesInner2ExpectedPoints[][2] = {{36,24}, {40,20}, {36,16}, {32,20}}; int twosOuterExpectedPoints[][2] = {{36,16}, {40,20}, {36,24}, {32,20}}; int threesOuterExpectedPoints[][2] = {{52,16}, {56,20}, {52,24}, {48,20}}; TestBoundaryPointsEqual(onesOuter->points, onesOuterExpectedPoints); TestBoundaryPointsEqual(onesInner0->points, onesInner0ExpectedPoints); TestBoundaryPointsEqual(onesInner2->points, onesInner2ExpectedPoints); TestBoundaryPointsEqual(twosOuter->points, twosOuterExpectedPoints); TestBoundaryPointsEqual(threesOuter->points, threesOuterExpectedPoints); } void test_nested_boundaries2() { Grid grid1 = GetGrid("-22222-" "-2---2-" "-2-1123" "-2-1123" "-2-2223" "-222333", 7, 6); std::vector boundaries = CTerritoryBoundaryCalculator::ComputeBoundaries(&grid1); // There should be two boundaries found for the territory of 2's (one outer and one inner edge), plus two regular // outer edges of the territories of 1's and 3's. The order in which they're returned doesn't matter though, so // we should first detect which one is which. size_t expectedNumBoundaries = 4; TS_ASSERT_EQUALS(expectedNumBoundaries, boundaries.size()); STerritoryBoundary* onesOuter = NULL; STerritoryBoundary* twosOuter = NULL; STerritoryBoundary* twosInner = NULL; STerritoryBoundary* threesOuter = NULL; for (size_t i=0; i < expectedNumBoundaries; i++) { STerritoryBoundary& boundary = boundaries[i]; switch (boundary.owner) { case 1: TSM_ASSERT_EQUALS("Too many boundaries for territory owned by player 1", onesOuter, (STerritoryBoundary*) NULL); onesOuter = &boundary; break; case 3: TSM_ASSERT_EQUALS("Too many boundaries for territory owned by player 3", threesOuter, (STerritoryBoundary*) NULL); threesOuter = &boundary; break; case 2: // assign twosOuter first, then twosInner last; we'll swap them afterwards if needed if (twosOuter == NULL) twosOuter = &boundary; else if (twosInner == NULL) twosInner = &boundary; else TS_FAIL("Too many boundaries for territory owned by player 2"); break; default: TS_FAIL("Unexpected tile owner"); break; } } TS_ASSERT_DIFFERS(onesOuter, (STerritoryBoundary*) NULL); TS_ASSERT_DIFFERS(twosOuter, (STerritoryBoundary*) NULL); TS_ASSERT_DIFFERS(twosInner, (STerritoryBoundary*) NULL); TS_ASSERT_DIFFERS(threesOuter, (STerritoryBoundary*) NULL); TS_ASSERT_EQUALS(onesOuter->points.size(), 8U); TS_ASSERT_EQUALS(twosOuter->points.size(), 22U); TS_ASSERT_EQUALS(twosInner->points.size(), 14U); TS_ASSERT_EQUALS(threesOuter->points.size(), 14U); // See if we need to swap the outer and inner edges of the twos territories (uses the extremely simplistic // heuristic of comparing the amount of points to determine which one is the outer one and which one the inner // one (which does happen to work in this case though). if (twosOuter->points.size() < twosInner->points.size()) { STerritoryBoundary* tmp = twosOuter; twosOuter = twosInner; twosInner = tmp; } int onesOuterExpectedPoints[][2] = {{28,16}, {36,16}, {40,20}, {40,28}, {36,32}, {28,32}, {24,28}, {24,20}}; int twosOuterExpectedPoints[][2] = {{12, 0}, {20, 0}, {28, 0}, {32, 4}, {36, 8}, {44, 8}, {48,12}, {48,20}, {48,28}, {48,36}, {48,44}, {44,48}, {36,48}, {28,48}, {20,48}, {12,48}, { 8,44}, { 8,36}, { 8,28}, { 8,20}, { 8,12}, { 8, 4}}; int twosInnerExpectedPoints[][2] = {{20,40}, {28,40}, {36,40}, {40,36}, {40,28}, {40,20}, {36,16}, {28,16}, {24,12}, {20, 8}, {16,12}, {16,20}, {16,28}, {16,36}}; int threesOuterExpectedPoints[][2] = {{36, 0}, {44, 0}, {52, 0}, {56, 4}, {56,12}, {56,20}, {56,28}, {52,32}, {48,28}, {48,20}, {48,12}, {44, 8}, {36, 8}, {32, 4}}; TestBoundaryPointsEqual(onesOuter->points, onesOuterExpectedPoints); TestBoundaryPointsEqual(twosOuter->points, twosOuterExpectedPoints); TestBoundaryPointsEqual(twosInner->points, twosInnerExpectedPoints); TestBoundaryPointsEqual(threesOuter->points, threesOuterExpectedPoints); } private: /// Parses a string representation of a grid into an actual Grid structure, such that the (i,j) axes are located in the bottom /// left hand side of the map. Note: leaves all custom bits in the grid values at zero (anything outside /// ICmpTerritoryManager::TERRITORY_PLAYER_MASK). Grid GetGrid(const std::string& def, u16 w, u16 h) { Grid grid(w, h); const char* chars = def.c_str(); for (u16 y=0; y& points, int expectedPoints[][2]) { // TODO: currently relies on an exact point match, i.e. expectedPoints must be specified going CCW or CW (depending on // whether we're testing an inner or an outer edge) starting from the exact same point that the algorithm happened to // decide to start the run from. This is an algorithmic detail and is not considered to be part of the specification // of the return value. Hence, this method should also accept 'expectedPoints' to be a cyclically shifted // version of 'points', so that the starting position doesn't need to match exactly. for (size_t i = 0; i < points.size(); i++) { // the input numbers in expectedPoints are defined under the assumption that NAVCELLS_PER_TERRITORY_TILE is 8, so let's include // a scaling factor to protect against that should NAVCELLS_PER_TERRITORY_TILE ever change TS_ASSERT_DELTA(points[i].X, float(expectedPoints[i][0]) * 8.f / ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE, 1e-7); TS_ASSERT_DELTA(points[i].Y, float(expectedPoints[i][1]) * 8.f / ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE, 1e-7); } } }; Index: ps/trunk/source/simulation2/components/tests/test_scripts.h =================================================================== --- ps/trunk/source/simulation2/components/tests/test_scripts.h (revision 27944) +++ ps/trunk/source/simulation2/components/tests/test_scripts.h (revision 27945) @@ -1,135 +1,135 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "simulation2/system/ComponentTest.h" #include "simulation2/serialization/StdDeserializer.h" #include "simulation2/serialization/StdSerializer.h" #include "ps/Filesystem.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptContext.h" class TestComponentScripts : public CxxTest::TestSuite { public: void setUp() { g_VFS = CreateVfs(); g_VFS->Mount(L"", DataDir() / "mods" / "mod" / "", VFS_MOUNT_MUST_EXIST); g_VFS->Mount(L"", DataDir() / "mods" / "public" / "", VFS_MOUNT_MUST_EXIST, 1); // ignore directory-not-found errors CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); } static void load_script(const ScriptInterface& scriptInterface, const VfsPath& pathname) { CVFSFile file; TS_ASSERT_EQUALS(file.Load(g_VFS, pathname), PSRETURN_OK); CStr content = file.DecodeUTF8(); // assume it's UTF-8 TSM_ASSERT(L"Running script "+pathname.string(), scriptInterface.LoadScript(pathname, content)); } static void Script_LoadComponentScript(const ScriptInterface& scriptInterface, const VfsPath& pathname) { ScriptRequest rq(scriptInterface); CComponentManager* componentManager = scriptInterface.ObjectFromCBData(rq); TS_ASSERT(componentManager->LoadScript(VfsPath(L"simulation/components") / pathname)); } static void Script_LoadHelperScript(const ScriptInterface& scriptInterface, const VfsPath& pathname) { ScriptRequest rq(scriptInterface); CComponentManager* componentManager = scriptInterface.ObjectFromCBData(rq); TS_ASSERT(componentManager->LoadScript(VfsPath(L"simulation/helpers") / pathname)); } static JS::Value Script_SerializationRoundTrip(const ScriptInterface& scriptInterface, JS::HandleValue value) { ScriptRequest rq(scriptInterface); JS::RootedValue val(rq.cx); val = value; std::stringstream stream; CStdSerializer serializer(scriptInterface, stream); serializer.ScriptVal("", &val); CStdDeserializer deserializer(scriptInterface, stream); deserializer.ScriptVal("", &val); return val; } void test_global_scripts() { if (!VfsDirectoryExists(L"globalscripts/tests/")) { debug_printf("Skipping globalscripts tests (can't find binaries/data/mods/public/globalscripts/tests/)\n"); return; } VfsPaths paths; TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"globalscripts/tests/", L"test_*.js", paths)); for (const VfsPath& path : paths) { CSimContext context; - CComponentManager componentManager(context, g_ScriptContext, true); + CComponentManager componentManager(context, *g_ScriptContext, true); ScriptTestSetup(componentManager.GetScriptInterface()); ScriptRequest rq(componentManager.GetScriptInterface()); ScriptFunction::Register(rq, "SerializationRoundTrip"); load_script(componentManager.GetScriptInterface(), path); } } void test_scripts() { if (!VfsFileExists(L"simulation/components/tests/setup.js")) { debug_printf("Skipping component scripts tests (can't find binaries/data/mods/public/simulation/components/tests/setup.js)\n"); return; } VfsPaths paths; TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"simulation/components/tests/", L"test_*.js", paths)); TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"simulation/helpers/tests/", L"test_*.js", paths)); paths.push_back(VfsPath(L"simulation/components/tests/setup_test.js")); for (const VfsPath& path : paths) { // Clean up previous scripts. g_ScriptContext->ShrinkingGC(); CSimContext context; - CComponentManager componentManager(context, g_ScriptContext, true); + CComponentManager componentManager(context, *g_ScriptContext, true); ScriptTestSetup(componentManager.GetScriptInterface()); ScriptRequest rq(componentManager.GetScriptInterface()); ScriptFunction::Register(rq, "LoadComponentScript"); ScriptFunction::Register(rq, "LoadHelperScript"); ScriptFunction::Register(rq, "SerializationRoundTrip"); componentManager.LoadComponentTypes(); load_script(componentManager.GetScriptInterface(), L"simulation/components/tests/setup.js"); load_script(componentManager.GetScriptInterface(), path); } } }; Index: ps/trunk/source/simulation2/system/ComponentManager.cpp =================================================================== --- ps/trunk/source/simulation2/system/ComponentManager.cpp (revision 27944) +++ ps/trunk/source/simulation2/system/ComponentManager.cpp (revision 27945) @@ -1,1165 +1,1165 @@ /* Copyright (C) 2023 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 "scriptinterface/FunctionWrapper.h" #include "scriptinterface/Object.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" #include /** * Used for script-only message types. */ class CMessageScripted final : 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 ScriptRequest& UNUSED(rq)) const { return msg.get(); } CMessageScripted(const ScriptRequest& rq, int mtid, const std::string& name, JS::HandleValue msg) : mtid(mtid), handlerName("On" + name), globalHandlerName("OnGlobal" + name), msg(rq.cx, msg) { } int mtid; std::string handlerName; std::string globalHandlerName; JS::PersistentRootedValue msg; }; -CComponentManager::CComponentManager(CSimContext& context, std::shared_ptr cx, bool skipScriptFunctions) : +CComponentManager::CComponentManager(CSimContext& context, ScriptContext& cx, bool skipScriptFunctions) : m_NextScriptComponentTypeId(CID__LastNative), m_ScriptInterface("Engine", "Simulation", cx), 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_ReadOnlySimulation(m_ScriptInterface); ScriptRequest rq(m_ScriptInterface); constexpr ScriptFunction::ObjectGetter Getter = &ScriptInterface::ObjectFromCBData; ScriptFunction::Register<&CComponentManager::Script_RegisterComponentType, Getter>(rq, "RegisterComponentType"); ScriptFunction::Register<&CComponentManager::Script_RegisterSystemComponentType, Getter>(rq, "RegisterSystemComponentType"); ScriptFunction::Register<&CComponentManager::Script_ReRegisterComponentType, Getter>(rq, "ReRegisterComponentType"); ScriptFunction::Register<&CComponentManager::Script_RegisterInterface, Getter>(rq, "RegisterInterface"); ScriptFunction::Register<&CComponentManager::Script_RegisterMessageType, Getter>(rq, "RegisterMessageType"); ScriptFunction::Register<&CComponentManager::Script_RegisterGlobal, Getter>(rq, "RegisterGlobal"); ScriptFunction::Register<&CComponentManager::Script_GetEntitiesWithInterface, Getter>(rq, "GetEntitiesWithInterface"); ScriptFunction::Register<&CComponentManager::Script_GetComponentsWithInterface, Getter>(rq, "GetComponentsWithInterface"); ScriptFunction::Register<&CComponentManager::Script_PostMessage, Getter>(rq, "PostMessage"); ScriptFunction::Register<&CComponentManager::Script_BroadcastMessage, Getter>(rq, "BroadcastMessage"); ScriptFunction::Register<&CComponentManager::Script_AddEntity, Getter>(rq, "AddEntity"); ScriptFunction::Register<&CComponentManager::Script_AddLocalEntity, Getter>(rq, "AddLocalEntity"); ScriptFunction::Register<&CComponentManager::QueryInterface, Getter>(rq, "QueryInterface"); ScriptFunction::Register<&CComponentManager::DestroyComponentsSoon, Getter>(rq, "DestroyEntity"); ScriptFunction::Register<&CComponentManager::FlushDestroyedComponents, Getter>(rq, "FlushDestroyedEntities"); ScriptFunction::Register<&CComponentManager::Script_GetTemplate, Getter>(rq, "GetTemplate"); } // 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(int iid, const std::string& cname, JS::HandleValue ctor, bool reRegister, bool systemComponent) { ScriptRequest rq(m_ScriptInterface); // Find the C++ component that wraps the interface int cidWrapper = GetScriptWrapper(iid); if (cidWrapper == CID__Invalid) { ScriptException::Raise(rq, "Invalid interface id"); return; } const ComponentType& ctWrapper = m_ComponentTypesById[cidWrapper]; bool mustReloadComponents = false; // for hotloading ComponentTypeId cid = LookupCID(cname); if (cid == CID__Invalid) { if (reRegister) { ScriptException::Raise(rq, "ReRegistering component type that was not registered before '%s'", cname.c_str()); return; } // Allocate a new cid number cid = m_NextScriptComponentTypeId++; m_ComponentTypeIdsByName[cname] = cid; if (systemComponent) MarkScriptedComponentForSystemEntity(cid); } else { // Component type is already loaded, so do hotloading: if (!m_CurrentlyHotloading && !reRegister) { ScriptException::Raise(rq, "Registering component type with already-registered name '%s'", cname.c_str()); return; } const ComponentType& ctPrevious = m_ComponentTypesById[cid]; // We can only replace scripted component types, not native ones if (ctPrevious.type != CT_Script) { ScriptException::Raise(rq, "Loading script component type with same name '%s' as native component", cname.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 (!m_ComponentsByTypeId[cid].empty()) { ScriptException::Raise(rq, "Hotloading script component type mustn't change interface ID"); return; } } // Remove the old component type's message subscriptions std::map >::iterator it; for (it = m_LocalMessageSubscriptions.begin(); it != 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 = m_GlobalMessageSubscriptions.begin(); it != 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 (!Script::GetProperty(rq, ctor, "prototype", &protoVal)) { ScriptException::Raise(rq, "Failed to get property 'prototype'"); return; } if (!protoVal.isObject()) { ScriptException::Raise(rq, "Component has no constructor"); return; } std::string schema = ""; if (Script::HasProperty(rq, protoVal, "Schema")) Script::GetProperty(rq, 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::make_unique(rq.cx, ctor) }; m_ComponentTypesById[cid] = std::move(ct); m_CurrentComponent = cid; // needed by Subscribe // Find all the ctor prototype's On* methods, and subscribe to the appropriate messages: std::vector methods; if (!Script::EnumeratePropertyNames(rq, protoVal, false, methods)) { ScriptException::Raise(rq, "Failed to enumerate component properties."); return; } for (const std::string& method : methods) { if (std::string_view{method}.substr(0, 2) != "On") continue; std::string_view name{std::string_view{method}.substr(2)}; // strip the "On" prefix // Handle "OnGlobalFoo" functions specially bool isGlobal = false; if (std::string_view{name}.substr(0, 6) == "Global") { isGlobal = true; name.remove_prefix(6); } auto mit = m_MessageTypeIdsByName.find(std::string{name}); if (mit == m_MessageTypeIdsByName.end()) { ScriptException::Raise(rq, "Registered component has unrecognized '%s' message handler method", method.c_str()); return; } if (isGlobal) SubscribeGloballyToMessageType(mit->second); else SubscribeToMessageType(mit->second); } 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 = 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()) m_ScriptInterface.SetPrototype(instance, protoVal); } } } void CComponentManager::Script_RegisterComponentType(int iid, const std::string& cname, JS::HandleValue ctor) { Script_RegisterComponentType_Common(iid, cname, ctor, false, false); m_ScriptInterface.SetGlobal(cname.c_str(), ctor, m_CurrentlyHotloading); } void CComponentManager::Script_RegisterSystemComponentType(int iid, const std::string& cname, JS::HandleValue ctor) { Script_RegisterComponentType_Common(iid, cname, ctor, false, true); m_ScriptInterface.SetGlobal(cname.c_str(), ctor, m_CurrentlyHotloading); } void CComponentManager::Script_ReRegisterComponentType(int iid, const std::string& cname, JS::HandleValue ctor) { Script_RegisterComponentType_Common(iid, cname, ctor, true, false); } void CComponentManager::Script_RegisterInterface(const std::string& name) { std::map::iterator it = m_InterfaceIdsByName.find(name); if (it != m_InterfaceIdsByName.end()) { // Redefinitions are fine (and just get ignored) when hotloading; otherwise // they're probably unintentional and should be reported if (!m_CurrentlyHotloading) { ScriptRequest rq(m_ScriptInterface); ScriptException::Raise(rq, "Registering interface with already-registered name '%s'", name.c_str()); } return; } // IIDs start at 1, so size+1 is the next unused one size_t id = m_InterfaceIdsByName.size() + 1; m_InterfaceIdsByName[name] = (InterfaceId)id; m_ComponentsByInterface.resize(id+1); // add one so we can index by InterfaceId m_ScriptInterface.SetGlobal(("IID_" + name).c_str(), (int)id); } void CComponentManager::Script_RegisterMessageType(const std::string& name) { std::map::iterator it = m_MessageTypeIdsByName.find(name); if (it != m_MessageTypeIdsByName.end()) { // Redefinitions are fine (and just get ignored) when hotloading; otherwise // they're probably unintentional and should be reported if (!m_CurrentlyHotloading) { ScriptRequest rq(m_ScriptInterface); ScriptException::Raise(rq, "Registering message type with already-registered name '%s'", name.c_str()); } return; } // MTIDs start at 1, so size+1 is the next unused one size_t id = m_MessageTypeIdsByName.size() + 1; RegisterMessageType((MessageTypeId)id, name.c_str()); m_ScriptInterface.SetGlobal(("MT_" + name).c_str(), (int)id); } void CComponentManager::Script_RegisterGlobal(const std::string& name, JS::HandleValue value) { m_ScriptInterface.SetGlobal(name.c_str(), value, m_CurrentlyHotloading); } const CParamNode& CComponentManager::Script_GetTemplate(const std::string& templateName) { static CParamNode nullNode(false); ICmpTemplateManager* cmpTemplateManager = static_cast (QueryInterface(SYSTEM_ENTITY, IID_TemplateManager)); if (!cmpTemplateManager) { LOGERROR("Template manager is not loaded"); return nullNode; } const CParamNode* tmpl = cmpTemplateManager->GetTemplate(templateName); if (!tmpl) return nullNode; return *tmpl; } std::vector CComponentManager::Script_GetEntitiesWithInterface(int iid) { std::vector ret; const InterfaceListUnordered& ents = 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(int iid) { std::vector ret; InterfaceList ents = 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); ScriptRequest rq(m_ScriptInterface); if (mtid < MT__LastNative) { return CMessageFromJSVal(mtid, rq, data); } else { return new CMessageScripted(rq, mtid, m_MessageTypeNamesById[mtid], data); } } void CComponentManager::Script_PostMessage(int ent, int mtid, JS::HandleValue data) { CMessage* msg = ConstructMessage(mtid, data); if (!msg) return; // error PostMessage(ent, *msg); delete msg; } void CComponentManager::Script_BroadcastMessage(int mtid, JS::HandleValue data) { CMessage* msg = ConstructMessage(mtid, data); if (!msg) return; // error BroadcastMessage(*msg); delete msg; } int CComponentManager::Script_AddEntity(const std::wstring& templateName) { // TODO: should validate the string to make sure it doesn't contain scary characters // that will let it access non-component-template files return AddEntity(templateName, AllocateNewEntity()); } int CComponentManager::Script_AddLocalEntity(const std::wstring& templateName) { // TODO: should validate the string to make sure it doesn't contain scary characters // that will let it access non-component-template files return AddEntity(templateName, AllocateNewLocalEntity()); } void CComponentManager::ResetState() { // Delete all dynamic message subscriptions m_DynamicMessageSubscriptionsNonsync.clear(); m_DynamicMessageSubscriptionsNonsyncByComponent.clear(); // Delete all IComponents in reverse order of creation. std::map >::reverse_iterator iit = m_ComponentsByTypeId.rbegin(); for (; iit != m_ComponentsByTypeId.rend(); ++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_UnitMotionManager, 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) { ScriptRequest 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 // NB: The unit motion manager relies on components not moving in memory once constructed. 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 cit = m_ComponentTypesById.find(*ctit); if (cit != m_ComponentTypesById.end() && cit->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/system/ComponentManager.h =================================================================== --- ps/trunk/source/simulation2/system/ComponentManager.h (revision 27944) +++ ps/trunk/source/simulation2/system/ComponentManager.h (revision 27945) @@ -1,340 +1,340 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 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 "simulation2/system/IComponent.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: using AllocFunc = IComponent::AllocFunc; using DeallocFunc = IComponent::DeallocFunc; // 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&, std::shared_ptr cx, bool skipScriptFunctions = false); + CComponentManager(CSimContext&, ScriptContext& cx, 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 void Script_RegisterComponentType_Common(int iid, const std::string& cname, JS::HandleValue ctor, bool reRegister, bool systemComponent); void Script_RegisterComponentType(int iid, const std::string& cname, JS::HandleValue ctor); void Script_RegisterSystemComponentType(int iid, const std::string& cname, JS::HandleValue ctor); void Script_ReRegisterComponentType(int iid, const std::string& cname, JS::HandleValue ctor); void Script_RegisterInterface(const std::string& name); void Script_RegisterMessageType(const std::string& name); void Script_RegisterGlobal(const std::string& name, JS::HandleValue value); std::vector Script_GetEntitiesWithInterface(int iid); std::vector Script_GetComponentsWithInterface(int iid); void Script_PostMessage(int ent, int mtid, JS::HandleValue data); void Script_BroadcastMessage(int mtid, JS::HandleValue data); int Script_AddEntity(const std::wstring& templateName); int Script_AddLocalEntity(const std::wstring& templateName); const CParamNode& Script_GetTemplate(const std::string& templateName); 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/system/ComponentTest.h =================================================================== --- ps/trunk/source/simulation2/system/ComponentTest.h (revision 27944) +++ ps/trunk/source/simulation2/system/ComponentTest.h (revision 27945) @@ -1,250 +1,250 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 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_COMPONENTTEST #define INCLUDED_COMPONENTTEST #include "lib/self_test.h" #include "maths/Vector3D.h" #include "ps/XML/Xeromyces.h" #include "simulation2/MessageTypes.h" #include "simulation2/system/Component.h" #include "simulation2/components/ICmpTerrain.h" #include "simulation2/serialization/DebugSerializer.h" #include "simulation2/serialization/HashSerializer.h" #include "simulation2/serialization/StdSerializer.h" #include "simulation2/serialization/StdDeserializer.h" #include /** * @file * Various common features for component test cases. */ /** * Class to test a single component. * - Create an instance of this class * - Use AddMock to add mock components that the tested component relies on * - Use Add to add the test component itself, and it returns a component pointer * - Call methods on the component pointer * - Use Roundtrip to test the consistency of serialization */ class ComponentTestHelper { CSimContext m_Context; CComponentManager m_ComponentManager; CParamNode m_Param; IComponent* m_Cmp; EComponentTypeId m_Cid; bool m_isSystemEntityInit = false; public: - ComponentTestHelper(std::shared_ptr scriptContext) : + ComponentTestHelper(ScriptContext& scriptContext) : m_Context(), m_ComponentManager(m_Context, scriptContext), m_Cmp(NULL) { m_ComponentManager.LoadComponentTypes(); } const ScriptInterface& GetScriptInterface() { return m_ComponentManager.GetScriptInterface(); } CSimContext& GetSimContext() { return m_Context; } /** * Call this once to initialise the test helper with a component. */ template T* Add(EComponentTypeId cid, const std::string& xml, entity_id_t ent = 10) { TS_ASSERT(m_Cmp == NULL); CEntityHandle handle; if (ent == SYSTEM_ENTITY) { if (!m_isSystemEntityInit) { m_ComponentManager.InitSystemEntity(); m_isSystemEntityInit = true; } handle = m_ComponentManager.GetSystemEntity(); m_Context.SetSystemEntity(handle); } else handle = m_ComponentManager.LookupEntityHandle(ent, true); m_Cid = cid; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(m_Param, ("" + xml + "").c_str()), PSRETURN_OK); TS_ASSERT(m_ComponentManager.AddComponent(handle, m_Cid, m_Param.GetChild("test"))); m_Cmp = m_ComponentManager.QueryInterface(ent, T::GetInterfaceId()); TS_ASSERT(m_Cmp != NULL); return static_cast (m_Cmp); } void AddMock(entity_id_t ent, EInterfaceId iid, IComponent& component) { CEntityHandle handle; if (ent == SYSTEM_ENTITY) { if (!m_isSystemEntityInit) { m_ComponentManager.InitSystemEntity(); m_isSystemEntityInit = true; } handle = m_ComponentManager.GetSystemEntity(); m_Context.SetSystemEntity(handle); } else handle = m_ComponentManager.LookupEntityHandle(ent, true); m_ComponentManager.AddMockComponent(handle, iid, component); } void HandleMessage(IComponent* cmp, const CMessage& msg, bool global) { cmp->HandleMessage(msg, global); } /** * Checks that the object roundtrips through its serialize/deserialize functions correctly. * Computes the debug output, hash, and binary serialization; then deserializes into a new * system and checks the serialization outputs are unchanged. */ void Roundtrip(bool verbose = false) { std::stringstream dbgstr1; CDebugSerializer dbg1(GetScriptInterface(), dbgstr1); m_Cmp->Serialize(dbg1); if (verbose) std::cout << "--------\n" << dbgstr1.str() << "--------\n"; CHashSerializer hash1(GetScriptInterface()); m_Cmp->Serialize(hash1); std::stringstream stdstr1; CStdSerializer std1(GetScriptInterface(), stdstr1); m_Cmp->Serialize(std1); ComponentTestHelper test2(GetScriptInterface().GetContext()); // (We should never need to add any mock objects etc to test2, since deserialization // mustn't depend on other components already existing) CEntityHandle ent = test2.m_ComponentManager.LookupEntityHandle(10, true); CStdDeserializer stdde2(test2.GetScriptInterface(), stdstr1); IComponent* cmp2 = test2.m_ComponentManager.ConstructComponent(ent, m_Cid); cmp2->Deserialize(m_Param.GetChild("test"), stdde2); TS_ASSERT(stdstr1.peek() == EOF); // Deserialize must read whole stream std::stringstream dbgstr2; CDebugSerializer dbg2(test2.GetScriptInterface(), dbgstr2); cmp2->Serialize(dbg2); if (verbose) std::cout << "--------\n" << dbgstr2.str() << "--------\n"; CHashSerializer hash2(test2.GetScriptInterface()); cmp2->Serialize(hash2); std::stringstream stdstr2; CStdSerializer std2(test2.GetScriptInterface(), stdstr2); cmp2->Serialize(std2); TS_ASSERT_EQUALS(dbgstr1.str(), dbgstr2.str()); TS_ASSERT_EQUALS(hash1.GetHashLength(), hash2.GetHashLength()); TS_ASSERT_SAME_DATA(hash1.ComputeHash(), hash2.ComputeHash(), hash1.GetHashLength()); TS_ASSERT_EQUALS(stdstr1.str(), stdstr2.str()); // TODO: need to extend this so callers can run methods on the cloned component // to check that all its data is still correct } }; /** * Simple terrain implementation with constant height of 50. */ class MockTerrain : public ICmpTerrain { public: DEFAULT_MOCK_COMPONENT() bool IsLoaded() const override { return true; } CFixedVector3D CalcNormal(entity_pos_t UNUSED(x), entity_pos_t UNUSED(z)) const override { return CFixedVector3D(fixed::FromInt(0), fixed::FromInt(1), fixed::FromInt(0)); } CVector3D CalcExactNormal(float UNUSED(x), float UNUSED(z)) const override { return CVector3D(0.f, 1.f, 0.f); } entity_pos_t GetGroundLevel(entity_pos_t UNUSED(x), entity_pos_t UNUSED(z)) const override { return entity_pos_t::FromInt(50); } float GetExactGroundLevel(float UNUSED(x), float UNUSED(z)) const override { return 50.f; } u16 GetTilesPerSide() const override { return 16; } u32 GetMapSize() const override { return GetTilesPerSide() * TERRAIN_TILE_SIZE; } u16 GetVerticesPerSide() const override { return 17; } CTerrain* GetCTerrain() override { return nullptr; } void MakeDirty(i32 UNUSED(i0), i32 UNUSED(j0), i32 UNUSED(i1), i32 UNUSED(j1)) override { } void ReloadTerrain(bool UNUSED(ReloadWater)) override { } }; #endif // INCLUDED_COMPONENTTEST Index: ps/trunk/source/simulation2/tests/test_CmpTemplateManager.h =================================================================== --- ps/trunk/source/simulation2/tests/test_CmpTemplateManager.h (revision 27944) +++ ps/trunk/source/simulation2/tests/test_CmpTemplateManager.h (revision 27945) @@ -1,259 +1,259 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2023 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" #include "scriptinterface/JSON.h" #include "scriptinterface/ScriptRequest.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" / "", 0, VFS_MAX_PRIORITY)); CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); } void test_LoadTemplate() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + 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_STR_EQUALS(basic->ToXMLString(), "12345"); const CParamNode* inherit2 = tempMan->LoadTemplate(ent2, "inherit2"); TS_ASSERT(inherit2 != NULL); TS_ASSERT_STR_EQUALS(inherit2->ToXMLString(), "d2e1f1g2"); const CParamNode* inherit1 = tempMan->LoadTemplate(ent2, "inherit1"); TS_ASSERT(inherit1 != NULL); TS_ASSERT_STR_EQUALS(inherit1->ToXMLString(), "d1e1f1"); const CParamNode* actor = tempMan->LoadTemplate(ent2, "actor|example1"); TS_ASSERT(actor != NULL); TS_ASSERT_STR_EQUALS(actor->ToXMLString(), "1.0128x128/ellipse.png128x128/ellipse_mask.png" "example1falsefalsefalse"); } void test_LoadTemplate_scriptcache() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + CComponentManager man(context, *g_ScriptContext); man.LoadComponentTypes(); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.LookupEntityHandle(ent1, true); CParamNode noParam; TS_ASSERT(man.AddComponent(hnd1, CID_TemplateManager, noParam)); ICmpTemplateManager* tempMan = static_cast (man.QueryInterface(ent1, IID_TemplateManager)); TS_ASSERT(tempMan != NULL); tempMan->DisableValidation(); ScriptRequest rq(man.GetScriptInterface()); // This is testing some bugs in the template JS object caching const CParamNode* inherit1 = tempMan->LoadTemplate(ent2, "inherit1"); JS::RootedValue val(rq.cx); Script::ToJSVal(rq, &val, inherit1); TS_ASSERT_STR_EQUALS(Script::ToString(rq, &val), "({Test1A:{'@a':\"a1\", '@b':\"b1\", '@c':\"c1\", d:\"d1\", e:\"e1\", f:\"f1\"}})"); const CParamNode* inherit2 = tempMan->LoadTemplate(ent2, "inherit2"); Script::ToJSVal(rq, &val, inherit2); TS_ASSERT_STR_EQUALS(Script::ToString(rq, &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"); Script::ToJSVal(rq, &val, &actor->GetChild("VisualActor")); TS_ASSERT_STR_EQUALS(Script::ToString(rq, &val), "({Actor:\"example1\", ActorOnly:(void 0), SilhouetteDisplay:\"false\", SilhouetteOccluder:\"false\", VisibleInAtlasOnly:\"false\"})"); const CParamNode* foundation = tempMan->LoadTemplate(ent2, "foundation|actor|example1"); Script::ToJSVal(rq, &val, &foundation->GetChild("VisualActor")); TS_ASSERT_STR_EQUALS(Script::ToString(rq, &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; \ Script::ToJSVal(rq, &val, it->second); \ break; \ } GET_FIRST_ELEMENT(n1, "inherit_a"); TS_ASSERT_STR_EQUALS(Script::ToString(rq, &val), "({'@datatype':\"tokens\", _string:\"a b c\"})"); GET_FIRST_ELEMENT(n2, "inherit_b"); TS_ASSERT_STR_EQUALS(Script::ToString(rq, &val), "({'@datatype':\"tokens\", _string:\"a b c d\"})"); GET_FIRST_ELEMENT(n3, "inherit_c"); TS_ASSERT_STR_EQUALS(Script::ToString(rq, &val), "({'@a':\"b\", _string:\"a b c\"})"); GET_FIRST_ELEMENT(n4, "inherit_d"); TS_ASSERT_STR_EQUALS(Script::ToString(rq, &val), "({'@a':\"b\", '@c':\"d\"})"); #undef GET_FIRST_ELEMENT } void test_LoadTemplate_errors() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + CComponentManager man(context, *g_ScriptContext); man.LoadComponentTypes(); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.LookupEntityHandle(ent1, true); CParamNode noParam; TS_ASSERT(man.AddComponent(hnd1, CID_TemplateManager, noParam)); ICmpTemplateManager* tempMan = static_cast (man.QueryInterface(ent1, IID_TemplateManager)); TS_ASSERT(tempMan != NULL); TestLogger logger; TS_ASSERT(tempMan->LoadTemplate(ent2, "illformed") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "invalid") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "nonexistent") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "inherit-loop") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "inherit-broken") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "inherit-special") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "preview|nonexistent") == NULL); } void test_LoadTemplate_multiple() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + 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" / "", 0, VFS_MAX_PRIORITY)); CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); } // This just attempts loading every public entity, to check there's no validation errors void test_load_all_DISABLED() // disabled since it's a bit slow and noisy { CTerrain dummy; CSimulation2 sim(NULL, g_ScriptContext, &dummy); sim.LoadDefaultScripts(); sim.ResetState(); CmpPtr cmpTemplateManager(sim, SYSTEM_ENTITY); TS_ASSERT(cmpTemplateManager); std::vector templates = cmpTemplateManager->FindAllTemplates(true); for (size_t i = 0; i < templates.size(); ++i) { std::string name = templates[i]; printf("# %s\n", name.c_str()); const CParamNode* p = cmpTemplateManager->GetTemplate(name); TS_ASSERT(p != NULL); } } }; Index: ps/trunk/source/simulation2/tests/test_ComponentManager.h =================================================================== --- ps/trunk/source/simulation2/tests/test_ComponentManager.h (revision 27944) +++ ps/trunk/source/simulation2/tests/test_ComponentManager.h (revision 27945) @@ -1,903 +1,903 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2023 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" / "", 0, VFS_MAX_PRIORITY)); CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); } void test_Load() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + CComponentManager man(context, *g_ScriptContext); man.LoadComponentTypes(); } void test_LookupCID() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + CComponentManager man(context, *g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT_EQUALS(man.LookupCID("Test1A"), (int)CID_Test1A); TS_ASSERT_EQUALS(man.LookupCID("Test1B"), (int)CID_Test1B); } void test_AllocateNewEntity() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + CComponentManager man(context, *g_ScriptContext); TS_ASSERT_EQUALS(man.AllocateNewEntity(), (u32)2); TS_ASSERT_EQUALS(man.AllocateNewEntity(), (u32)3); TS_ASSERT_EQUALS(man.AllocateNewEntity(), (u32)4); TS_ASSERT_EQUALS(man.AllocateNewEntity(100), (u32)100); TS_ASSERT_EQUALS(man.AllocateNewEntity(), (u32)101); // TODO: // TS_ASSERT_EQUALS(man.AllocateNewEntity(3), (u32)102); TS_ASSERT_EQUALS(man.AllocateNewLocalEntity(), (u32)FIRST_LOCAL_ENTITY); TS_ASSERT_EQUALS(man.AllocateNewLocalEntity(), (u32)FIRST_LOCAL_ENTITY+1); man.ResetState(); TS_ASSERT_EQUALS(man.AllocateNewEntity(), (u32)2); TS_ASSERT_EQUALS(man.AllocateNewEntity(3), (u32)3); TS_ASSERT_EQUALS(man.AllocateNewLocalEntity(), (u32)FIRST_LOCAL_ENTITY); } void test_rng() { // Ensure we get the same random number with the same seed double first; { CSimContext context; - CComponentManager man(context, g_ScriptContext); + CComponentManager man(context, *g_ScriptContext); man.SetRNGSeed(123); if (!man.m_ScriptInterface.MathRandom(first)) TS_FAIL("Couldn't get random number!"); } double second; { CSimContext context; - CComponentManager man(context, g_ScriptContext); + CComponentManager man(context, *g_ScriptContext); man.SetRNGSeed(123); if (!man.m_ScriptInterface.MathRandom(second)) TS_FAIL("Couldn't get random number!"); } TS_ASSERT_EQUALS(first, second); } void test_AddComponent_errors() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + CComponentManager man(context, *g_ScriptContext); man.LoadComponentTypes(); CEntityHandle hnd1 = man.AllocateEntityHandle(1); CParamNode noParam; TS_ASSERT(man.AddComponent(hnd1, CID_Test1A, noParam)); { TestLogger log; TS_ASSERT(! man.AddComponent(hnd1, 12345, noParam)); TS_ASSERT_STR_CONTAINS(log.GetOutput(), "ERROR: Invalid component id 12345"); } { TestLogger log; TS_ASSERT(! man.AddComponent(hnd1, CID_Test1B, noParam)); TS_ASSERT_STR_CONTAINS(log.GetOutput(), "ERROR: Multiple components for interface "); } } void test_QueryInterface() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + CComponentManager man(context, *g_ScriptContext); man.LoadComponentTypes(); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CParamNode noParam; man.AddComponent(hnd1, CID_Test1A, noParam); TS_ASSERT(man.QueryInterface(ent1, IID_Test1) != NULL); TS_ASSERT(man.QueryInterface(ent1, IID_Test2) == NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test1) == NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test2) == NULL); man.AddComponent(hnd2, CID_Test1B, noParam); TS_ASSERT(man.QueryInterface(ent2, IID_Test1) != NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test2) == NULL); man.AddComponent(hnd2, CID_Test2A, noParam); TS_ASSERT(man.QueryInterface(ent2, IID_Test1) != NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test2) != NULL); } void test_SendMessage() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + CComponentManager man(context, *g_ScriptContext); man.LoadComponentTypes(); entity_id_t ent1 = 1, ent2 = 2, ent3 = 3, ent4 = 4; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CEntityHandle hnd3 = man.AllocateEntityHandle(ent3); CEntityHandle hnd4 = man.AllocateEntityHandle(ent4); CParamNode noParam; man.AddComponent(hnd1, CID_Test1A, noParam); man.AddComponent(hnd2, CID_Test1B, noParam); man.AddComponent(hnd3, CID_Test2A, noParam); man.AddComponent(hnd4, CID_Test1A, noParam); man.AddComponent(hnd4, CID_Test2A, noParam); CMessageTurnStart msg1; CMessageUpdate msg2(fixed::FromInt(100)); CMessageInterpolate msg3(0, 0, 0); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 12000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test2))->GetX(), 21000); // Test_1A subscribed locally to msg1, nothing subscribed globally man.PostMessage(ent1, msg1); man.PostMessage(ent1, msg2); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 12000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test2))->GetX(), 21000); man.BroadcastMessage(msg1); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11002); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 12000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21050); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test1))->GetX(), 11001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test2))->GetX(), 21050); // Test_1B, Test_2A subscribed locally to msg2, nothing subscribed globally man.BroadcastMessage(msg2); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11002); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 12010); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21150); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test1))->GetX(), 11001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test2))->GetX(), 21150); // Test_1A subscribed locally to msg3, Test_1B subscribed globally man.BroadcastMessage(msg3); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11004); // local TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 12030); // global TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21150); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test1))->GetX(), 11003); // local TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test2))->GetX(), 21150); man.PostMessage(ent1, msg3); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11006); // local TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 12050); // global TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21150); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test1))->GetX(), 11003); // local - skipped TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test2))->GetX(), 21150); } void test_ParamNode() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + CComponentManager man(context, *g_ScriptContext); man.LoadComponentTypes(); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CParamNode noParam; CParamNode testParam; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(testParam, "1234"), PSRETURN_OK); man.AddComponent(hnd1, CID_Test1A, noParam); man.AddComponent(hnd2, CID_Test1A, testParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 1234); } void test_script_basic() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + CComponentManager man(context, *g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test.js")); TS_ASSERT_EQUALS(man.LookupCID("TestScript1A"), (int)CID__LastNative); TS_ASSERT_EQUALS(man.LookupCID("TestScript1B"), (int)CID__LastNative+1); entity_id_t ent1 = 1, ent2 = 2, ent3 = 3; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CEntityHandle hnd3 = man.AllocateEntityHandle(ent3); CParamNode noParam; man.AddComponent(hnd1, CID_Test1A, noParam); man.AddComponent(hnd2, man.LookupCID("TestScript1A"), noParam); man.AddComponent(hnd3, man.LookupCID("TestScript1B"), noParam); man.AddComponent(hnd3, man.LookupCID("TestScript2A"), noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 101000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test1))->GetX(), 102000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 201000); CMessageTurnStart msg1; CMessageUpdate msg2(fixed::FromInt(25)); man.BroadcastMessage(msg1); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 101001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test1))->GetX(), 102001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 201000); man.BroadcastMessage(msg2); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 101001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test1))->GetX(), 102001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 201025); } void test_script_helper_basic() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + CComponentManager man(context, *g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-helper.js")); TS_ASSERT(man.LoadScript(L"simulation/helpers/test-helper.js")); entity_id_t ent1 = 1; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; man.AddComponent(hnd1, man.LookupCID("TestScript1_Helper"), noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 3); } void test_script_global_helper() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + CComponentManager man(context, *g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-global-helper.js")); entity_id_t ent1 = 1; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; man.AddComponent(hnd1, man.LookupCID("TestScript1_GlobalHelper"), noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 2); } void test_script_interface() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + CComponentManager man(context, *g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/interfaces/test-interface.js")); TS_ASSERT(man.LoadScript(L"simulation/components/test-interface.js")); entity_id_t ent1 = 1; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; man.AddComponent(hnd1, man.LookupCID("TestScript1_Interface"), noParam); man.AddComponent(hnd1, man.LookupCID("TestScript2_Interface"), noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 1000 + IID__LastNative); } void test_script_errors() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + CComponentManager man(context, *g_ScriptContext); ScriptTestSetup(man.m_ScriptInterface); man.LoadComponentTypes(); TestLogger log; TS_ASSERT(man.LoadScript(L"simulation/components/error.js")); // The following exception is caught and dropped by the JS script, and should not appear in the logs. TS_ASSERT_STR_NOT_CONTAINS(log.GetOutput(), "ERROR: JavaScript error: simulation/components/error.js line 4\nInvalid interface id"); // The following exception is not caught by the JS script. TS_ASSERT_STR_CONTAINS(log.GetOutput(), "ERROR: No script wrapper found for interface id 12345"); } void test_script_entityID() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + CComponentManager man(context, *g_ScriptContext); ScriptTestSetup(man.m_ScriptInterface); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-entityid.js")); entity_id_t ent1 = 1, ent2 = 234; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CParamNode noParam; man.AddComponent(hnd1, man.LookupCID("TestScript1A"), noParam); man.AddComponent(hnd2, man.LookupCID("TestScript1A"), noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), (int)ent1); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), (int)ent2); } void test_script_QueryInterface() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + CComponentManager man(context, *g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-query.js")); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CParamNode noParam; man.AddComponent(hnd1, man.LookupCID("TestScript1A"), noParam); man.AddComponent(hnd1, man.LookupCID("TestScript2A"), noParam); man.AddComponent(hnd2, man.LookupCID("TestScript1A"), noParam); man.AddComponent(hnd2, CID_Test2A, noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 400); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 21000); } void test_script_AddEntity() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + CComponentManager man(context, *g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-addentity.js")); TS_ASSERT(man.LoadScript(L"simulation/components/addentity/test-addentity.js")); man.InitSystemEntity(); entity_id_t ent1 = man.AllocateNewEntity(); entity_id_t ent2 = ent1 + 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; TS_ASSERT(man.AddComponent(man.GetSystemEntity(), CID_TemplateManager, noParam)); TS_ASSERT(man.AddComponent(hnd1, man.LookupCID("TestScript1_AddEntity"), noParam)); TS_ASSERT(man.QueryInterface(ent2, IID_Test1) == NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test2) == NULL); { TestLogger logger; // ignore bogus-template warnings TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), (int)ent2); } TS_ASSERT(man.QueryInterface(ent2, IID_Test1) != NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test2) != NULL); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 999); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test2))->GetX(), 12345); } void test_script_AddLocalEntity() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + CComponentManager man(context, *g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-addentity.js")); TS_ASSERT(man.LoadScript(L"simulation/components/addentity/test-addentity.js")); man.InitSystemEntity(); entity_id_t ent1 = man.AllocateNewEntity(); entity_id_t ent2 = man.AllocateNewLocalEntity() + 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; TS_ASSERT(man.AddComponent(man.GetSystemEntity(), CID_TemplateManager, noParam)); TS_ASSERT(man.AddComponent(hnd1, man.LookupCID("TestScript1_AddLocalEntity"), noParam)); TS_ASSERT(man.QueryInterface(ent2, IID_Test1) == NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test2) == NULL); { TestLogger logger; // ignore bogus-template warnings TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), (int)ent2); } TS_ASSERT(man.QueryInterface(ent2, IID_Test1) != NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test2) != NULL); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 999); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test2))->GetX(), 12345); } void test_script_DestroyEntity() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + CComponentManager man(context, *g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-destroyentity.js")); entity_id_t ent1 = 10; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; TS_ASSERT(man.AddComponent(hnd1, man.LookupCID("TestScript1_DestroyEntity"), noParam)); TS_ASSERT(man.QueryInterface(ent1, IID_Test1) != NULL); static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(); TS_ASSERT(man.QueryInterface(ent1, IID_Test1) != NULL); man.FlushDestroyedComponents(); TS_ASSERT(man.QueryInterface(ent1, IID_Test1) == NULL); } void test_script_messages() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + CComponentManager man(context, *g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-msg.js")); entity_id_t ent1 = 1, ent2 = 2, ent3 = 3; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CEntityHandle hnd3 = man.AllocateEntityHandle(ent3); CParamNode noParam; man.AddComponent(hnd1, man.LookupCID("TestScript1A"), noParam); man.AddComponent(hnd1, man.LookupCID("TestScript2A"), noParam); man.AddComponent(hnd2, man.LookupCID("TestScript1A"), noParam); man.AddComponent(hnd2, CID_Test2A, noParam); man.AddComponent(hnd3, man.LookupCID("TestScript1B"), noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 100); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 100); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test2))->GetX(), 21000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test1))->GetX(), 100); // This GetX broadcasts messages TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test2))->GetX(), 200); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 650); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 5150); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test2))->GetX(), 26050); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test1))->GetX(), 5650); } void test_script_template() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + CComponentManager man(context, *g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-param.js")); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CParamNode noParam; CParamNode testParam; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(testParam, "1110000"), PSRETURN_OK); man.AddComponent(hnd1, man.LookupCID("TestScript1_Init"), noParam); man.AddComponent(hnd2, man.LookupCID("TestScript1_Init"), testParam.GetChild("node")); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 100); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 1+10+100+1000); } void test_script_template_readonly() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + CComponentManager man(context, *g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-param.js")); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CParamNode noParam; CParamNode testParam; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(testParam, "100"), PSRETURN_OK); man.AddComponent(hnd1, man.LookupCID("TestScript1_readonly"), testParam); man.AddComponent(hnd2, man.LookupCID("TestScript1_readonly"), testParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 102); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 102); } void test_script_hotload() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + CComponentManager man(context, *g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-hotload1.js")); entity_id_t ent1 = 1, ent2 = 2, ent3 = 3, ent4 = 4; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CEntityHandle hnd3 = man.AllocateEntityHandle(ent3); CEntityHandle hnd4 = man.AllocateEntityHandle(ent4); CParamNode testParam; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(testParam, "100"), PSRETURN_OK); man.AddComponent(hnd1, man.LookupCID("HotloadA"), testParam); man.AddComponent(hnd2, man.LookupCID("HotloadB"), testParam); man.AddComponent(hnd2, man.LookupCID("HotloadC"), testParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 100); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 200); TS_ASSERT(man.LoadScript(L"simulation/components/test-hotload2.js", true)); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 1000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 200); man.AddComponent(hnd3, man.LookupCID("HotloadA"), testParam); man.AddComponent(hnd4, man.LookupCID("HotloadB"), testParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test1))->GetX(), 1000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test1))->GetX(), 200); } void test_script_modding() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + CComponentManager man(context, *g_ScriptContext); man.LoadComponentTypes(); CParamNode testParam; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(testParam, "100"), PSRETURN_OK); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); TS_ASSERT(man.LoadScript(L"simulation/components/test-modding1.js")); man.AddComponent(hnd1, man.LookupCID("Modding"), testParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 100); TS_ASSERT(man.LoadScript(L"simulation/components/test-modding2.js")); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 1000); man.AddComponent(hnd2, man.LookupCID("Modding"), testParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 1000); } void test_serialization() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + CComponentManager man(context, *g_ScriptContext); man.LoadComponentTypes(); entity_id_t ent1 = 10, ent2 = 20, ent3 = FIRST_LOCAL_ENTITY; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CEntityHandle hnd3 = man.AllocateEntityHandle(ent3); CParamNode noParam; CParamNode testParam; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(testParam, "1234"), PSRETURN_OK); man.AddComponent(hnd1, CID_Test1A, noParam); man.AddComponent(hnd1, CID_Test2A, noParam); man.AddComponent(hnd2, CID_Test1A, testParam); man.AddComponent(hnd3, CID_Test2A, noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test2))->GetX(), 21000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 1234); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21000); std::stringstream debugStream; TS_ASSERT(man.DumpDebugState(debugStream, true)); TS_ASSERT_STR_EQUALS(debugStream.str(), "rng: \"78606\"\n" "entities:\n" "- id: 10\n" " Test1A:\n" " x: 11000\n" " Test2A:\n" " x: 21000\n" "\n" "- id: 20\n" " Test1A:\n" " x: 1234\n" "\n" "- id: 536870912\n" " type: local\n" " Test2A:\n" " x: 21000\n" "\n" ); std::string hash; TS_ASSERT(man.ComputeStateHash(hash, false)); TS_ASSERT_EQUALS(hash.length(), (size_t)16); TS_ASSERT_SAME_DATA(hash.data(), "\x3c\x25\x6e\x22\x58\x23\x09\x58\x38\xca\xb2\x1e\x0b\x8c\xac\xcf", 16); // echo -en "\x05\x00\x00\x0078606\x02\0\0\0\x01\0\0\0\x0a\0\0\0\xf8\x2a\0\0\x14\0\0\0\xd2\x04\0\0\x04\0\0\0\x0a\0\0\0\x08\x52\0\0" | md5sum | perl -pe 's/([0-9a-f]{2})/\\x$1/g' // ^^^^^^^^ rng ^^^^^^^^ ^^next^^ ^^Test1A^^ ^^^ent1^^ ^^^11000^^^ ^^^ent2^^ ^^^1234^^^ ^^Test2A^^ ^^ent1^^ ^^^21000^^^ std::stringstream stateStream; TS_ASSERT(man.SerializeState(stateStream)); TS_ASSERT_STREAM(stateStream, 73, "\x05\x00\x00\x00\x37\x38\x36\x30\x36" // RNG "\x02\x00\x00\x00" // next entity ID "\x00\x00\x00\x00" // num system component types "\x02\x00\x00\x00" // num component types "\x06\x00\x00\x00Test1A" "\x02\x00\x00\x00" // num ents "\x0a\x00\x00\x00" // ent1 "\xf8\x2a\x00\x00" // 11000 "\x14\x00\x00\x00" // ent2 "\xd2\x04\x00\x00" // 1234 "\x06\x00\x00\x00Test2A" "\x01\x00\x00\x00" // num ents "\x0a\x00\x00\x00" // ent1 "\x08\x52\x00\x00" // 21000 ); CSimContext context2; - CComponentManager man2(context2, g_ScriptContext); + CComponentManager man2(context2, *g_ScriptContext); man2.LoadComponentTypes(); TS_ASSERT(man2.QueryInterface(ent1, IID_Test1) == NULL); TS_ASSERT(man2.QueryInterface(ent1, IID_Test2) == NULL); TS_ASSERT(man2.QueryInterface(ent2, IID_Test1) == NULL); TS_ASSERT(man2.QueryInterface(ent3, IID_Test2) == NULL); TS_ASSERT(man2.DeserializeState(stateStream)); TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent1, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent1, IID_Test2))->GetX(), 21000); TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent2, IID_Test1))->GetX(), 1234); TS_ASSERT(man2.QueryInterface(ent3, IID_Test2) == NULL); } void test_script_serialization() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + CComponentManager man(context, *g_ScriptContext); ScriptTestSetup(man.m_ScriptInterface); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-serialize.js")); entity_id_t ent1 = 1, ent2 = 2, ent3 = 3, ent4 = 4; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CEntityHandle hnd3 = man.AllocateEntityHandle(ent3); CEntityHandle hnd4 = man.AllocateEntityHandle(ent4); CParamNode noParam; CParamNode testParam; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(testParam, "1234"), PSRETURN_OK); man.AddComponent(hnd1, man.LookupCID("TestScript1_values"), testParam); man.AddComponent(hnd2, man.LookupCID("TestScript1_entity"), testParam); // TODO: Since the upgrade to SpiderMonkey v24 this test won't be able to correctly represent // non-tree structures because sharp variables were removed (bug 566700). // This also affects the debug serializer and it could make sense to implement correct serialization again. man.AddComponent(hnd3, man.LookupCID("TestScript1_nontree"), testParam); man.AddComponent(hnd4, man.LookupCID("TestScript1_custom"), testParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 1234); { TestLogger log; // swallow warnings about this.entity being read-only TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), (int)ent2); } TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test1))->GetX(), 8); std::stringstream debugStream; TS_ASSERT(man.DumpDebugState(debugStream, true)); TS_ASSERT_STR_EQUALS(debugStream.str(), "rng: \"78606\"\n\ entities:\n\ - id: 1\n\ TestScript1_values:\n\ comp: {\n\ \"x\": 1234,\n\ \"str\": \"this is a string\",\n\ \"things\": {\n\ \"a\": 1,\n\ \"b\": \"2\",\n\ \"c\": [\n\ 3,\n\ \"4\",\n\ [\n\ 5,\n\ []\n\ ]\n\ ]\n\ }\n\ }\n\ \n\ - id: 2\n\ TestScript1_entity:\n\ comp: {}\n\ \n\ - id: 3\n\ TestScript1_nontree:\n\ comp: ({x:[[2], [2], [], {y:[2]}]})\n\ \n\ - id: 4\n\ TestScript1_custom:\n\ comp: {\n\ \"c\": 1\n\ }\n\ \n" ); std::stringstream stateStream; TS_ASSERT(man.SerializeState(stateStream)); CSimContext context2; - CComponentManager man2(context2, g_ScriptContext); + CComponentManager man2(context2, *g_ScriptContext); man2.LoadComponentTypes(); TS_ASSERT(man2.LoadScript(L"simulation/components/test-serialize.js")); TS_ASSERT(man2.QueryInterface(ent1, IID_Test1) == NULL); TS_ASSERT(man2.DeserializeState(stateStream)); TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent1, IID_Test1))->GetX(), 1234); { TestLogger log; // swallow warnings about this.entity being read-only TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent2, IID_Test1))->GetX(), (int)ent2); } TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent3, IID_Test1))->GetX(), 12); } void test_script_serialization_errors() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + 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); TestLogger log; std::stringstream stateStream; TS_ASSERT_THROWS_PSERROR(man.SerializeState(stateStream), PSERROR_Serialize_ScriptError, "Cannot serialize property getters"); // (The script will die if the getter is executed) } void test_script_serialization_template() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + CComponentManager man(context, *g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-serialize.js")); man.InitSystemEntity(); entity_id_t ent2 = 2; CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CParamNode noParam; // The template manager takes care of reloading templates on deserialization, // so we need to use it here TS_ASSERT(man.AddComponent(man.GetSystemEntity(), CID_TemplateManager, noParam)); ICmpTemplateManager* tempMan = static_cast (man.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager)); const CParamNode* testParam = tempMan->LoadTemplate(ent2, "template-serialize"); man.AddComponent(hnd2, man.LookupCID("TestScript1_consts"), testParam->GetChild("TestScript1_consts")); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 12347); std::stringstream stateStream; TS_ASSERT(man.SerializeState(stateStream)); CSimContext context2; - CComponentManager man2(context2, g_ScriptContext); + CComponentManager man2(context2, *g_ScriptContext); man2.LoadComponentTypes(); TS_ASSERT(man2.LoadScript(L"simulation/components/test-serialize.js")); TS_ASSERT(man2.DeserializeState(stateStream)); TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent2, IID_Test1))->GetX(), 12347); } void test_dynamic_subscription() { CSimContext context; - CComponentManager man(context, g_ScriptContext); + 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_Serializer.h =================================================================== --- ps/trunk/source/simulation2/tests/test_Serializer.h (revision 27944) +++ ps/trunk/source/simulation2/tests/test_Serializer.h (revision 27945) @@ -1,941 +1,941 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 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/FunctionWrapper.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptInterface.h" #include "graphics/MapReader.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureManager.h" #include "lib/config2.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" #if CONFIG2_VALGRIND # include "callgrind.h" #endif #include #define TS_ASSERT_STREAM(stream, len, buffer) \ TS_ASSERT_EQUALS(stream.str().length(), (size_t)len); \ TS_ASSERT_SAME_DATA(stream.str().data(), buffer, len) #define TSM_ASSERT_STREAM(m, stream, len, buffer) \ TSM_ASSERT_EQUALS(m, stream.str().length(), (size_t)len); \ TSM_ASSERT_SAME_DATA(m, stream.str().data(), buffer, len) class TestSerializer : public CxxTest::TestSuite { public: void serialize_types(ISerializer& serialize) { serialize.NumberI8_Unbounded("i8", (signed char)-123); serialize.NumberU8_Unbounded("u8", (unsigned char)255); serialize.NumberI16_Unbounded("i16", -12345); serialize.NumberU16_Unbounded("u16", 56789); serialize.NumberI32_Unbounded("i32", -123); serialize.NumberU32_Unbounded("u32", (unsigned)-123); serialize.NumberFloat_Unbounded("float", 1e+30f); serialize.NumberDouble_Unbounded("double", 1e+300); serialize.NumberFixed_Unbounded("fixed", fixed::FromFloat(1234.5f)); serialize.Bool("t", true); serialize.Bool("f", false); serialize.StringASCII("string", "example", 0, 255); serialize.StringASCII("string 2", "example\"\\\"", 0, 255); serialize.StringASCII("string 3", "example\n\ntest", 0, 255); wchar_t testw[] = { 't', 0xEA, 's', 't', 0 }; serialize.String("string 4", testw, 0, 255); serialize.RawBytes("raw bytes", (const u8*)"\0\1\2\3\x0f\x10", 6); } void test_Debug_basic() { ScriptInterface script("Test", "Test", g_ScriptContext); std::stringstream stream; CDebugSerializer serialize(script, stream); serialize.NumberI32_Unbounded("x", -123); serialize.NumberU32_Unbounded("y", 1234); serialize.NumberI32("z", 12345, 0, 65535); TS_ASSERT_STR_EQUALS(stream.str(), "x: -123\ny: 1234\nz: 12345\n"); } void test_Debug_floats() { ScriptInterface script("Test", "Test", g_ScriptContext); std::stringstream stream; CDebugSerializer serialize(script, stream); serialize.NumberFloat_Unbounded("x", 1e4f); serialize.NumberFloat_Unbounded("x", 1e-4f); serialize.NumberFloat_Unbounded("x", 1e5f); serialize.NumberFloat_Unbounded("x", 1e-5f); serialize.NumberFloat_Unbounded("x", 1e6f); serialize.NumberFloat_Unbounded("x", 1e-6f); serialize.NumberFloat_Unbounded("x", 1e10f); serialize.NumberFloat_Unbounded("x", 1e-10f); serialize.NumberDouble_Unbounded("x", 1e4); serialize.NumberDouble_Unbounded("x", 1e-4); serialize.NumberDouble_Unbounded("x", 1e5); serialize.NumberDouble_Unbounded("x", 1e-5); serialize.NumberDouble_Unbounded("x", 1e6); serialize.NumberDouble_Unbounded("x", 1e-6); serialize.NumberDouble_Unbounded("x", 1e10); serialize.NumberDouble_Unbounded("x", 1e-10); serialize.NumberDouble_Unbounded("x", 1e100); serialize.NumberDouble_Unbounded("x", 1e-100); serialize.NumberFixed_Unbounded("x", fixed::FromDouble(1e4)); TS_ASSERT_STR_EQUALS(stream.str(), "x: 10000\nx: 9.9999997e-05\nx: 100000\nx: 9.9999997e-06\nx: 1000000\nx: 1e-06\nx: 1e+10\nx: 1e-10\n" "x: 10000\nx: 0.0001\nx: 100000\nx: 1.0000000000000001e-05\nx: 1000000\nx: 9.9999999999999995e-07\nx: 10000000000\nx: 1e-10\nx: 1e+100\nx: 1e-100\n" "x: 10000\n" ); } void test_Debug_types() { ScriptInterface script("Test", "Test", g_ScriptContext); std::stringstream stream; CDebugSerializer serialize(script, stream); serialize.Comment("comment"); serialize_types(serialize); TS_ASSERT_STR_EQUALS(stream.str(), "# comment\n" "i8: -123\n" "u8: 255\n" "i16: -12345\n" "u16: 56789\n" "i32: -123\n" "u32: 4294967173\n" "float: 1e+30\n" "double: 1.0000000000000001e+300\n" "fixed: 1234.5\n" "t: true\n" "f: false\n" "string: \"example\"\n" "string 2: \"example\\\"\\\\\\\"\"\n" // C-escaped form of: "example\"\\\"" "string 3: \"example\\n\\ntest\"\n" "string 4: \"t\xC3\xAAst\"\n" "raw bytes: (6 bytes) 00 01 02 03 0f 10\n" ); } void test_Std_basic() { ScriptInterface script("Test", "Test", g_ScriptContext); std::stringstream stream; CStdSerializer serialize(script, stream); serialize.NumberI32_Unbounded("x", -123); serialize.NumberU32_Unbounded("y", 1234); serialize.NumberI32("z", 12345, 0, 65535); TS_ASSERT_STREAM(stream, 12, "\x85\xff\xff\xff" "\xd2\x04\x00\x00" "\x39\x30\x00\x00"); CStdDeserializer deserialize(script, stream); int32_t n; deserialize.NumberI32_Unbounded("x", n); TS_ASSERT_EQUALS(n, -123); deserialize.NumberI32_Unbounded("y", n); TS_ASSERT_EQUALS(n, 1234); deserialize.NumberI32("z", n, 0, 65535); TS_ASSERT_EQUALS(n, 12345); // NOTE: Don't use good() here - it fails due to a bug in older libc++ versions TS_ASSERT(!stream.bad() && !stream.fail()); TS_ASSERT_EQUALS(stream.peek(), EOF); } void test_Std_types() { ScriptInterface script("Test", "Test", g_ScriptContext); std::stringstream stream; CStdSerializer serialize(script, stream); serialize_types(serialize); CStdDeserializer deserialize(script, stream); int8_t i8v; uint8_t u8v; int16_t i16v; uint16_t u16v; int32_t i32v; uint32_t u32v; float flt; double dbl; fixed fxd; bool bl; std::string str; std::wstring wstr; u8 cbuf[256]; deserialize.NumberI8_Unbounded("i8", i8v); TS_ASSERT_EQUALS(i8v, -123); deserialize.NumberU8_Unbounded("u8", u8v); TS_ASSERT_EQUALS(u8v, 255); deserialize.NumberI16_Unbounded("i16", i16v); TS_ASSERT_EQUALS(i16v, -12345); deserialize.NumberU16_Unbounded("u16", u16v); TS_ASSERT_EQUALS(u16v, 56789); deserialize.NumberI32_Unbounded("i32", i32v); TS_ASSERT_EQUALS(i32v, -123); deserialize.NumberU32_Unbounded("u32", u32v); TS_ASSERT_EQUALS(u32v, 4294967173u); deserialize.NumberFloat_Unbounded("float", flt); TS_ASSERT_EQUALS(flt, 1e+30f); deserialize.NumberDouble_Unbounded("double", dbl); TS_ASSERT_EQUALS(dbl, 1e+300); deserialize.NumberFixed_Unbounded("fixed", fxd); TS_ASSERT_EQUALS(fxd.ToDouble(), 1234.5); deserialize.Bool("t", bl); TS_ASSERT_EQUALS(bl, true); deserialize.Bool("f", bl); TS_ASSERT_EQUALS(bl, false); deserialize.StringASCII("string", str, 0, 255); TS_ASSERT_STR_EQUALS(str, "example"); deserialize.StringASCII("string 2", str, 0, 255); TS_ASSERT_STR_EQUALS(str, "example\"\\\""); deserialize.StringASCII("string 3", str, 0, 255); TS_ASSERT_STR_EQUALS(str, "example\n\ntest"); wchar_t testw[] = { 't', 0xEA, 's', 't', 0 }; deserialize.String("string 4", wstr, 0, 255); TS_ASSERT_WSTR_EQUALS(wstr, testw); cbuf[6] = 0x42; // sentinel deserialize.RawBytes("raw bytes", cbuf, 6); TS_ASSERT_SAME_DATA(cbuf, (const u8*)"\0\1\2\3\x0f\x10\x42", 7); // NOTE: Don't use good() here - it fails due to a bug in older libc++ versions TS_ASSERT(!stream.bad() && !stream.fail()); TS_ASSERT_EQUALS(stream.peek(), EOF); } void test_Hash_basic() { ScriptInterface script("Test", "Test", g_ScriptContext); CHashSerializer serialize(script); serialize.NumberI32_Unbounded("x", -123); serialize.NumberU32_Unbounded("y", 1234); serialize.NumberI32("z", 12345, 0, 65535); TS_ASSERT_EQUALS(serialize.GetHashLength(), (size_t)16); TS_ASSERT_SAME_DATA(serialize.ComputeHash(), "\xa0\x3a\xe5\x3e\x9b\xd7\xfb\x11\x88\x35\xc6\xfb\xb9\x94\xa9\x72", 16); // echo -en "\x85\xff\xff\xff\xd2\x04\x00\x00\x39\x30\x00\x00" | openssl md5 -binary | xxd -p | perl -pe 's/(..)/\\x$1/g' } void test_Hash_stream() { ScriptInterface script("Test", "Test", g_ScriptContext); CHashSerializer hashSerialize(script); hashSerialize.NumberI32_Unbounded("x", -123); hashSerialize.NumberU32_Unbounded("y", 1234); hashSerialize.NumberI32("z", 12345, 0, 65535); ISerializer& serialize = hashSerialize; { CStdSerializer streamSerialize(script, serialize.GetStream()); streamSerialize.NumberI32_Unbounded("x2", -456); streamSerialize.NumberU32_Unbounded("y2", 5678); streamSerialize.NumberI32("z2", 45678, 0, 65535); } TS_ASSERT_EQUALS(hashSerialize.GetHashLength(), (size_t)16); TS_ASSERT_SAME_DATA(hashSerialize.ComputeHash(), "\x5c\xff\x33\xd1\x72\xdd\x6d\x77\xa8\xd4\xa1\xf6\x84\xcc\xaa\x10", 16); // echo -en "\x85\xff\xff\xff\xd2\x04\x00\x00\x39\x30\x00\x00\x38\xfe\xff\xff\x2e\x16\x00\x00\x6e\xb2\x00\x00" | openssl md5 -binary | xxd -p | perl -pe 's/(..)/\\x$1/g' } void test_bounds() { ScriptInterface script("Test", "Test", g_ScriptContext); std::stringstream stream; CDebugSerializer serialize(script, stream); serialize.NumberI32("x", 16, -16, 16); serialize.NumberI32("x", -16, -16, 16); TS_ASSERT_THROWS(serialize.NumberI32("x", 99, -16, 16), const PSERROR_Serialize_OutOfBounds&); TS_ASSERT_THROWS(serialize.NumberI32("x", -17, -16, 16), const PSERROR_Serialize_OutOfBounds&); } // TODO: test exceptions more thoroughly void helper_script_roundtrip(const char* msg, const char* input, const char* expected, size_t expstreamlen = 0, const char* expstream = NULL, const char* debug = NULL) { ScriptInterface script("Test", "Test", g_ScriptContext); ScriptRequest rq(script); JS::RootedValue obj(rq.cx); TSM_ASSERT(msg, script.Eval(input, &obj)); if (debug) { std::stringstream dbgstream; CDebugSerializer serialize(script, dbgstream); serialize.ScriptVal("script", &obj); TS_ASSERT_STR_EQUALS(dbgstream.str(), debug); } std::stringstream stream; CStdSerializer serialize(script, stream); serialize.ScriptVal("script", &obj); if (expstream) { TSM_ASSERT_STREAM(msg, stream, expstreamlen, expstream); } CStdDeserializer deserialize(script, stream); JS::RootedValue newobj(rq.cx); deserialize.ScriptVal("script", &newobj); // NOTE: Don't use good() here - it fails due to a bug in older libc++ versions TSM_ASSERT(msg, !stream.bad() && !stream.fail()); TSM_ASSERT_EQUALS(msg, stream.peek(), EOF); std::stringstream stream2; CStdSerializer serialize2(script, stream2); CStdDeserializer deserialize2(script, stream2); // Round-trip the deserialized value again. This helps ensure prototypes are correctly deserialized. serialize2.ScriptVal("script2", &newobj); deserialize2.ScriptVal("script2", &newobj); std::string source; TSM_ASSERT(msg, ScriptFunction::Call(rq, newobj, "toSource", source)); TS_ASSERT_STR_EQUALS(source, expected); } void test_script_basic() { helper_script_roundtrip("Object", "({'x': 123, 'y': [1, 1.5, '2', 'test', undefined, null, true, false]})", /* expected: */ "({x:123, y:[1, 1.5, \"2\", \"test\", (void 0), null, true, false]})", /* expected stream: */ 116, "\x03" // SCRIPT_TYPE_OBJECT "\x02\0\0\0" // num props "\x01\x01\0\0\0" "x" // "x" "\x05" // SCRIPT_TYPE_INT "\x7b\0\0\0" // 123 "\x01\x01\0\0\0" "y" // "y" "\x02" // SCRIPT_TYPE_ARRAY "\x08\0\0\0" // array length "\x08\0\0\0" // num props "\x01\x01\0\0\0" "0" // "0" "\x05" "\x01\0\0\0" // SCRIPT_TYPE_INT 1 "\x01\x01\0\0\0" "1" // "1" "\x06" "\0\0\0\0\0\0\xf8\x3f" // SCRIPT_TYPE_DOUBLE 1.5 "\x01\x01\0\0\0" "2" // "2" "\x04" "\x01\x01\0\0\0" "2" // SCRIPT_TYPE_STRING "2" "\x01\x01\0\0\0" "3" // "3" "\x04" "\x01\x04\0\0\0" "test" // SCRIPT_TYPE_STRING "test" "\x01\x01\0\0\0" "4" // "4" "\x00" // SCRIPT_TYPE_VOID "\x01\x01\0\0\0" "5" // "5" "\x01" // SCRIPT_TYPE_NULL "\x01\x01\0\0\0" "6" // "6" "\x07" "\x01" // SCRIPT_TYPE_BOOLEAN true "\x01\x01\0\0\0" "7" // "7" "\x07" "\x00", // SCRIPT_TYPE_BOOLEAN false /* expected debug: */ "script: {\n" " \"x\": 123,\n" " \"y\": [\n" " 1,\n" " 1.5,\n" " \"2\",\n" " \"test\",\n" " null,\n" " null,\n" " true,\n" " false\n" " ]\n" "}\n" ); } void test_script_unicode() { helper_script_roundtrip("unicode", "({" "'x': \"\\x01\\x80\\xff\\u0100\\ud7ff\", " "'y': \"\\ue000\\ufffd\"" "})", /* expected: */ "({" "x:\"\\x01\\x80\\xFF\\u0100\\uD7FF\", " "y:\"\\uE000\\uFFFD\"" "})"); // Disabled since we no longer do the UTF-8 conversion that rejects invalid characters // TS_ASSERT_THROWS(helper_script_roundtrip("invalid chars 1", "(\"\\ud7ff\\ud800\")", "..."), PSERROR_Serialize_InvalidCharInString); // TS_ASSERT_THROWS(helper_script_roundtrip("invalid chars 2", "(\"\\udfff\")", "..."), PSERROR_Serialize_InvalidCharInString); // TS_ASSERT_THROWS(helper_script_roundtrip("invalid chars 3", "(\"\\uffff\")", "..."), PSERROR_Serialize_InvalidCharInString); // TS_ASSERT_THROWS(helper_script_roundtrip("invalid chars 4", "(\"\\ud800\\udc00\")" /* U+10000 */, "..."), PSERROR_Serialize_InvalidCharInString); helper_script_roundtrip("unicode", "\"\\ud800\\uffff\"", "(new String(\"\\uD800\\uFFFF\"))"); } void test_script_objects() { helper_script_roundtrip("Number", "[1, new Number('2.0'), 3]", "[1, (new Number(2)), 3]"); helper_script_roundtrip("Number with props", "var n=new Number('2.0'); n.foo='bar'; n", "(new Number(2))"); helper_script_roundtrip("String", "['test1', new String('test2'), 'test3']", "[\"test1\", (new String(\"test2\")), \"test3\"]"); helper_script_roundtrip("String with props", "var s=new String('test'); s.foo='bar'; s", "(new String(\"test\"))"); helper_script_roundtrip("Boolean", "[new Boolean('true'), false]", "[(new Boolean(true)), false]"); helper_script_roundtrip("Boolean with props", "var b=new Boolean('true'); b.foo='bar'; b", "(new Boolean(true))"); } void test_script_fancy_objects() { // This asserts that objects are deserialized with their correct prototypes. helper_script_roundtrip("Custom Object", "" "function customObj() { this.a = this.customFunc.name; };" "customObj.prototype.customFunc = function customFunc(){};" "new customObj();", "({a:\"customFunc\"})"); helper_script_roundtrip("Custom Class", "" "class customObj {" " constructor() { this.a = this.customFunc.name; }" " customFunc(){};" "}; new customObj();", "({a:\"customFunc\"})"); helper_script_roundtrip("Custom Class with Serialize/Deserialize()", "" "class customObj {" " constructor() { this.a = this.customFunc.name; }" " Serialize() { return { 'foo': 'bar' }; }" " Deserialize(data) { this.foo = data.foo; }" " customFunc(){};" "}; new customObj();", "({a:\"customFunc\", foo:\"bar\"})"); helper_script_roundtrip("Custom Class with null serialize & deserialize()", "" "class customObj {" " constructor() { this.a = this.customFunc.name; }" " Deserialize(data) { this.test = 'test'; };" " customFunc(){};" "}; customObj.prototype.Serialize=null;" "new customObj();", "({a:\"customFunc\", test:\"test\"})"); helper_script_roundtrip("Custom Class with arguments but still works", "" "class customObj {" " constructor(test) { this.a = test; }" " Serialize() { return { 'data': this.a }; };" " Deserialize(data) { this.a = data.data; };" "}; new customObj(4);", "({a:4})"); } void test_script_objects_properties() { helper_script_roundtrip("Object with null in prop name", "({\"foo\\0bar\":1})", "({\'foo\\x00bar\':1})"); } void test_script_typed_arrays_simple() { helper_script_roundtrip("Int8Array", "var arr=new Int8Array(8);" "for(var i=0; iMount(L"", DataDir() / "mods" / "public" / "", VFS_MOUNT_MUST_EXIST)); TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir() / "_testcache" / "", 0, VFS_MAX_PRIORITY)); CTerrain terrain; CSimulation2 sim2(NULL, g_ScriptContext, &terrain); sim2.LoadDefaultScripts(); sim2.ResetState(); std::unique_ptr mapReader = std::make_unique(); LDR_BeginRegistering(); mapReader->LoadMap(L"maps/skirmishes/Greek Acropolis (2).pmp", - *sim2.GetScriptInterface().GetContext(), JS::UndefinedHandleValue, + sim2.GetScriptInterface().GetContext(), JS::UndefinedHandleValue, &terrain, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &sim2, &sim2.GetSimContext(), -1, false); LDR_EndRegistering(); TS_ASSERT_OK(LDR_NonprogressiveLoad()); sim2.Update(0); { std::stringstream str; std::string hash; sim2.SerializeState(str); sim2.ComputeStateHash(hash, false); debug_printf("\n"); debug_printf("# size = %d\n", (int)str.str().length()); debug_printf("# hash = "); for (size_t i = 0; i < hash.size(); ++i) debug_printf("%02x", (unsigned int)(u8)hash[i]); debug_printf("\n"); } double t = timer_Time(); #if CONFIG2_VALGRIND CALLGRIND_START_INSTRUMENTATION; #endif size_t reps = 128; for (size_t i = 0; i < reps; ++i) { std::string hash; sim2.ComputeStateHash(hash, false); } #if CONFIG2_VALGRIND CALLGRIND_STOP_INSTRUMENTATION; #endif t = timer_Time() - t; debug_printf("# time = %f (%f/%d)\n", t/reps, t, (int)reps); // Shut down the world g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); CXeromyces::Terminate(); } };