From 262c5c037ef698ca5bb0f06d848202a82025f08a Mon Sep 17 00:00:00 2001 From: phosit Date: Wed, 7 May 2025 11:24:39 +0200 Subject: [PATCH] Use promises to fetch net messages Refs: #5585 --- .../mods/public/autostart/autostart_client.js | 44 ++++----- .../mods/public/autostart/autostart_host.js | 90 ++++++++++--------- .../gui/gamesetup/NetMessages/NetMessages.js | 6 +- .../mods/public/gui/gamesetup/SetupWindow.js | 1 - .../mods/public/gui/gamesetup/gamesetup.js | 2 + .../public/gui/gamesetup_mp/gamesetup_mp.js | 8 +- .../data/mods/public/gui/session/messages.js | 6 +- .../data/mods/public/gui/session/session.js | 4 +- source/gui/CGUI.cpp | 9 +- source/network/NetClient.cpp | 43 +++++++-- source/network/NetClient.h | 32 +++++-- .../network/scripting/JSInterface_Network.cpp | 9 +- source/ps/GameSetup/GameSetup.cpp | 1 + 13 files changed, 159 insertions(+), 96 deletions(-) diff --git a/binaries/data/mods/public/autostart/autostart_client.js b/binaries/data/mods/public/autostart/autostart_client.js index 472f35d20b..b3f49de13d 100644 --- a/binaries/data/mods/public/autostart/autostart_client.js +++ b/binaries/data/mods/public/autostart/autostart_client.js @@ -1,5 +1,6 @@ class AutoStartClient { + done = false; constructor(cmdLineArgs) { this.playerAssignments = {}; @@ -16,30 +17,33 @@ class AutoStartClient const message = sprintf(translate("Cannot join game: %(message)s."), { "message": e.message }); messageBox(400, 200, message, translate("Error")); } + + (async() => + { + while (true) + { + const message = await Engine.PollNetworkClient(); + + switch (message.type) + { + case "players": + this.playerAssignments = message.newAssignments; + Engine.SendNetworkReady(2); + break; + case "start": + this.onLaunch(message); + // Process further pending netmessages in the session page. + this.done = true; + return; + default: + } + } + })(); } onTick() { - while (true) - { - const message = Engine.PollNetworkClient(); - if (!message) - break; - - switch (message.type) - { - case "players": - this.playerAssignments = message.newAssignments; - Engine.SendNetworkReady(2); - break; - case "start": - this.onLaunch(message); - // Process further pending netmessages in the session page. - return true; - default: - } - } - return false; + return this.done; } /** diff --git a/binaries/data/mods/public/autostart/autostart_host.js b/binaries/data/mods/public/autostart/autostart_host.js index 26606ad76d..9209df6a19 100644 --- a/binaries/data/mods/public/autostart/autostart_host.js +++ b/binaries/data/mods/public/autostart/autostart_host.js @@ -1,5 +1,6 @@ class AutoStartHost { + done = false; constructor(cmdLineArgs) { this.launched = false; @@ -21,52 +22,57 @@ class AutoStartHost const message = sprintf(translate("Cannot host game: %(message)s."), { "message": e.message }); messageBox(400, 200, message, translate("Error")); } + + /** + * Handles a simple implementation of player assignments. + * Should not need be overloaded in mods unless you want to change that logic. + */ + (async() => + { + while (true) + { + const message = await Engine.PollNetworkClient(); + switch (message.type) + { + case "players": + { + this.playerAssignments = message.newAssignments; + Engine.SendNetworkReady(2); + let max = 0; + for (const uid in this.playerAssignments) + { + max = Math.max(this.playerAssignments[uid].player, max); + if (this.playerAssignments[uid].player == -1) + Engine.AssignNetworkPlayer(++max, uid); + } + break; + } + case "ready": + this.playerAssignments[message.guid].status = message.status; + break; + case "start": + this.done = true; + return; + default: + } + + if (!this.launched) + { + const assignementArray = Object.values(this.playerAssignments); + if (assignementArray.length === this.maxPlayers && + assignementArray.every(assignement => + assignement.player !== -1 || assignement.status !== 0)) + { + this.onLaunch(); + } + } + } + })(); } - /** - * Handles a simple implementation of player assignments. - * Should not need be overloaded in mods unless you want to change that logic. - */ onTick() { - while (true) - { - const message = Engine.PollNetworkClient(); - if (!message) - break; - - switch (message.type) - { - case "players": - { - this.playerAssignments = message.newAssignments; - Engine.SendNetworkReady(2); - let max = 0; - for (const uid in this.playerAssignments) - { - max = Math.max(this.playerAssignments[uid].player, max); - if (this.playerAssignments[uid].player == -1) - Engine.AssignNetworkPlayer(++max, uid); - } - break; - } - case "ready": - this.playerAssignments[message.guid].status = message.status; - break; - case "start": - return true; - default: - } - } - - if (!this.launched && Object.keys(this.playerAssignments).length == this.maxPlayers) - { - for (const uid in this.playerAssignments) - if (this.playerAssignments[uid].player == -1 || this.playerAssignments[uid].status == 0) - return false; - this.onLaunch(); - } - return false; + return this.done; } /** diff --git a/binaries/data/mods/public/gui/gamesetup/NetMessages/NetMessages.js b/binaries/data/mods/public/gui/gamesetup/NetMessages/NetMessages.js index 02cdd5cf11..9d276c2331 100644 --- a/binaries/data/mods/public/gui/gamesetup/NetMessages/NetMessages.js +++ b/binaries/data/mods/public/gui/gamesetup/NetMessages/NetMessages.js @@ -27,13 +27,11 @@ class NetMessages error("Unknown net message type: " + uneval(messageType)); } - pollPendingMessages() + async pollPendingMessages() { while (true) { - const message = Engine.PollNetworkClient(); - if (!message) - break; + const message = await Engine.PollNetworkClient(); log("Net message: " + uneval(message)); diff --git a/binaries/data/mods/public/gui/gamesetup/SetupWindow.js b/binaries/data/mods/public/gui/gamesetup/SetupWindow.js index 23afe5dbe4..d29eb01767 100644 --- a/binaries/data/mods/public/gui/gamesetup/SetupWindow.js +++ b/binaries/data/mods/public/gui/gamesetup/SetupWindow.js @@ -110,7 +110,6 @@ class SetupWindow onTick() { - this.controls.netMessages.pollPendingMessages(); updateTimers(); } diff --git a/binaries/data/mods/public/gui/gamesetup/gamesetup.js b/binaries/data/mods/public/gui/gamesetup/gamesetup.js index 2efdab8ee2..2d7e18fcb0 100644 --- a/binaries/data/mods/public/gui/gamesetup/gamesetup.js +++ b/binaries/data/mods/public/gui/gamesetup/gamesetup.js @@ -44,6 +44,8 @@ var g_SetupWindow; function init(initData, hotloadData) { g_SetupWindow = new SetupWindow(initData, hotloadData); + return g_IsNetworked ? g_SetupWindow.controls.netMessages.pollPendingMessages() : + new Promise(() => {}); } function getHotloadData() diff --git a/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js b/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js index 0b2d824ed0..c65fc5a8f5 100644 --- a/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js +++ b/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js @@ -243,13 +243,13 @@ function reportConnectionFail(reason) ); } -function pollAndHandleNetworkClient(loadSavedGame) +async function pollAndHandleNetworkClient(loadSavedGame) { while (true) { - var message = Engine.PollNetworkClient(); - if (!message) - return false; + const message = await Engine.PollNetworkClient(); + if (!g_IsConnecting) + continue; log(sprintf("Net message: %(message)s", { "message": uneval(message) })); // If we're rejoining an active game, we don't want to actually display diff --git a/binaries/data/mods/public/gui/session/messages.js b/binaries/data/mods/public/gui/session/messages.js index 4b9bf16e70..4bf3f9c862 100644 --- a/binaries/data/mods/public/gui/session/messages.js +++ b/binaries/data/mods/public/gui/session/messages.js @@ -417,13 +417,11 @@ function updateTutorial(notification) * Process every CNetMessage (see NetMessage.h, NetMessages.h) sent by the CNetServer. * Saves the received object to mainlog.html. */ -function handleNetMessages() +async function handleNetMessages() { while (true) { - const msg = Engine.PollNetworkClient(); - if (!msg) - return; + const msg = await Engine.PollNetworkClient(); log("Net message: " + uneval(msg)); diff --git a/binaries/data/mods/public/gui/session/session.js b/binaries/data/mods/public/gui/session/session.js index c3b437ef4d..587a711ffe 100644 --- a/binaries/data/mods/public/gui/session/session.js +++ b/binaries/data/mods/public/gui/session/session.js @@ -345,6 +345,8 @@ function init(initData, hotloadData) setTimeout(displayGamestateNotifications, 1000); + if (g_IsNetworked) + return Promise.race([promise, handleNetMessages()]); return promise; } @@ -630,8 +632,6 @@ function onTick() const tickLength = now - g_LastTickTime; g_LastTickTime = now; - handleNetMessages(); - updateCursorAndTooltip(); updateTimers(); diff --git a/source/gui/CGUI.cpp b/source/gui/CGUI.cpp index 736d192359..59e8f39d99 100644 --- a/source/gui/CGUI.cpp +++ b/source/gui/CGUI.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2025 Wildfire Games. +/* Copyright (C) 2026 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -41,6 +41,7 @@ #include "lib/timer.h" #include "lib/utf8.h" #include "maths/Size2D.h" +#include "network/NetClient.h" #include "ps/CLogger.h" #include "ps/Errors.h" #include "ps/Filesystem.h" @@ -104,7 +105,11 @@ CGUI::CGUI(ScriptContext& context) m_ScriptInterface->LoadGlobalScripts(); } -CGUI::~CGUI() = default; +CGUI::~CGUI() +{ + if (g_NetClient) + g_NetClient->Unregister(*m_ScriptInterface); +} InReaction CGUI::HandleEvent(const SDL_Event_* ev) { diff --git a/source/network/NetClient.cpp b/source/network/NetClient.cpp index b682820e8a..4e53851613 100644 --- a/source/network/NetClient.cpp +++ b/source/network/NetClient.cpp @@ -40,6 +40,7 @@ #include "ps/Profile.h" #include "ps/Threading.h" #include "scriptinterface/JSON.h" +#include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" #include "simulation2/system/TurnManager.h" @@ -49,6 +50,7 @@ #include #include #include +#include #include #include #include @@ -323,6 +325,7 @@ void CNetClient::Poll() CheckServerConnection(); m_Session->ProcessPolledMessages(); + FetchMessage(); } void CNetClient::CheckServerConnection() @@ -357,15 +360,35 @@ void CNetClient::CheckServerConnection() } } -JS::Value CNetClient::GuiPoll(const ScriptRequest& rq) +JSObject* CNetClient::GetNextGUIMessage(const ScriptInterface& guiInterface) { - if (m_GuiMessageQueue.empty()) - return JS::UndefinedValue(); + const ScriptRequest rq{guiInterface}; + m_GuiMessagePoll.emplace(GuiPollData{guiInterface, {rq.cx, JS::NewPromiseObject(rq.cx, nullptr)}}); - JS::RootedValue ret{rq.cx}; - Script::ReadStructuredClone(rq, m_GuiMessageQueue.front(), &ret); + FetchMessage(); + return m_GuiMessagePoll.value().promise; +} + +void CNetClient::Unregister(const ScriptInterface& guiInterface) +{ + if (m_GuiMessagePoll.has_value() && &m_GuiMessagePoll.value().interface == &guiInterface) + m_GuiMessagePoll.reset(); +} + +void CNetClient::FetchMessage() +{ + if (m_GuiMessageQueue.empty() || !m_GuiMessagePoll.has_value() || + JS::GetPromiseState(m_GuiMessagePoll.value().promise) != JS::PromiseState::Pending) + { + return; + } + + const ScriptRequest rq{m_GuiMessagePoll.value().interface}; + JS::RootedValue message{rq.cx}; + Script::ReadStructuredClone(rq, std::move(m_GuiMessageQueue.front()), &message); m_GuiMessageQueue.pop_front(); - return ret; + + JS::ResolvePromise(rq.cx, m_GuiMessagePoll.value().promise, message); } std::string CNetClient::TestReadGuiMessages() @@ -375,9 +398,13 @@ std::string CNetClient::TestReadGuiMessages() std::string r; while (true) { - JS::RootedValue msg{rq.cx, GuiPoll(rq)}; - if (msg.isUndefined()) + JS::RootedObject promise{rq.cx, GetNextGUIMessage(GetScriptInterface())}; + g_ScriptContext->RunJobs(); + + if (JS::GetPromiseState(promise) == JS::PromiseState::Pending) break; + + JS::RootedValue msg{rq.cx, JS::GetPromiseResult(promise)}; r += Script::ToString(rq, &msg) + "\n"; } return r; diff --git a/source/network/NetClient.h b/source/network/NetClient.h index 37cf5047e5..374b78413d 100644 --- a/source/network/NetClient.h +++ b/source/network/NetClient.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2025 Wildfire Games. +/* Copyright (C) 2026 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -144,19 +144,25 @@ public: /** * Retrieves the next queued GUI message, and removes it from the queue. - * The returned value is in the GetScriptInterface() JS context. + * The returned value is in the JS context of the provided + * @c ScriptInterface. * * This is the only mechanism for the networking code to send messages to - * the GUI - it is pull-based (instead of push) so the engine code does not - * need to know anything about the code structure of the GUI scripts. + * the GUI. * * The structure of the messages is { "type": "...", ... }. * The exact types and associated data are not specified anywhere - the * implementation and GUI scripts must make the same assumptions. * - * @return next message, or the value 'undefined' if the queue is empty + * @return a promise resolving to the next message. */ - JS::Value GuiPoll(const ScriptRequest& rq); + JSObject* GetNextGUIMessage(const ScriptInterface& guiInterface); + + /** + * Has to be called bevore the @c ScriptInterface gets destroied so that + * no future messages are sent to it. + */ + void Unregister(const ScriptInterface& guiInterface); /** * Add a message to the queue, to be read by GuiPoll. @@ -305,6 +311,8 @@ private: */ void PostPlayerAssignmentsToScript(); + void FetchMessage(); + CGame *m_Game; CStrW m_UserName; @@ -346,6 +354,18 @@ private: /// Queue of messages for GuiPoll std::deque m_GuiMessageQueue; + struct GuiPollData + { + const ScriptInterface& interface; + /** + * In the context of interface. + * When the promise is pending @see Poll should fill it with a message. + * When there it's fulfilled JavaScript code can take it. + */ + JS::PersistentRootedObject promise; + }; + std::optional m_GuiMessagePoll; + /// Serialized game state received when joining an in-progress game std::string m_JoinSyncBuffer; diff --git a/source/network/scripting/JSInterface_Network.cpp b/source/network/scripting/JSInterface_Network.cpp index 86dd2e4a47..e35aaf47b7 100644 --- a/source/network/scripting/JSInterface_Network.cpp +++ b/source/network/scripting/JSInterface_Network.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2025 Wildfire Games. +/* Copyright (C) 2026 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -196,9 +196,12 @@ CStr GetPlayerGUID() return g_NetClient->GetGUID(); } -JS::Value PollNetworkClient(const ScriptRequest& rq) +JS::Value PollNetworkClient(const ScriptInterface& guiInterface) { - return g_NetClient ? g_NetClient->GuiPoll(rq) : JS::UndefinedValue(); + if (!g_NetClient) + throw std::logic_error{"Network client not present"}; + + return JS::ObjectValue(*g_NetClient->GetNextGUIMessage(guiInterface)); } void SendGameSetupMessage(const ScriptInterface& scriptInterface, JS::HandleValue attribs1) diff --git a/source/ps/GameSetup/GameSetup.cpp b/source/ps/GameSetup/GameSetup.cpp index c9668c9288..7fa8c0bc68 100644 --- a/source/ps/GameSetup/GameSetup.cpp +++ b/source/ps/GameSetup/GameSetup.cpp @@ -855,6 +855,7 @@ bool Autostart(const CmdLineArgs& args) while (!shouldQuit) { g_NetClient->Poll(); + g_ScriptContext->RunJobs(); if (!ScriptFunction::Call(rq, global, "onTick", shouldQuit)) return false; std::this_thread::sleep_for(std::chrono::microseconds(200));