Use promises to fetch net messages

Refs: #5585
This commit is contained in:
phosit 2025-05-07 11:24:39 +02:00
parent 56107e4e39
commit 262c5c037e
No known key found for this signature in database
GPG key ID: C9430B600671C268
13 changed files with 159 additions and 96 deletions

View file

@ -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;
}
/**

View file

@ -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;
}
/**

View file

@ -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));

View file

@ -110,7 +110,6 @@ class SetupWindow
onTick()
{
this.controls.netMessages.pollPendingMessages();
updateTimers();
}

View file

@ -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()

View file

@ -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

View file

@ -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));

View file

@ -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();

View file

@ -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)
{

View file

@ -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 <functional>
#include <iterator>
#include <js/GCAPI.h>
#include <js/Promise.h>
#include <js/TracingAPI.h>
#include <map>
#include <memory>
@ -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;

View file

@ -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 <code>{ "type": "...", ... }</code>.
* 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<Script::StructuredClone> 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<GuiPollData> m_GuiMessagePoll;
/// Serialized game state received when joining an in-progress game
std::string m_JoinSyncBuffer;

View file

@ -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)

View file

@ -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));