Index: ps/trunk/source/gui/ObjectBases/IGUIObject.cpp
===================================================================
--- ps/trunk/source/gui/ObjectBases/IGUIObject.cpp (revision 24168)
+++ ps/trunk/source/gui/ObjectBases/IGUIObject.cpp (revision 24169)
@@ -1,541 +1,541 @@
/* Copyright (C) 2019 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "IGUIObject.h"
#include "gui/CGUI.h"
#include "gui/CGUISetting.h"
#include "js/Conversions.h"
#include "ps/CLogger.h"
#include "ps/GameSetup/Config.h"
#include "ps/Profile.h"
#include "scriptinterface/ScriptInterface.h"
#include "soundmanager/ISoundManager.h"
const CStr IGUIObject::EventNameMouseEnter = "MouseEnter";
const CStr IGUIObject::EventNameMouseMove = "MouseMove";
const CStr IGUIObject::EventNameMouseLeave = "MouseLeave";
IGUIObject::IGUIObject(CGUI& pGUI)
: m_pGUI(pGUI),
m_pParent(),
m_MouseHovering(),
m_LastClickTime(),
m_Enabled(),
m_Hidden(),
m_Size(),
m_Style(),
m_Hotkey(),
m_Z(),
m_Absolute(),
m_Ghost(),
m_AspectRatio(),
m_Tooltip(),
m_TooltipStyle()
{
RegisterSetting("enabled", m_Enabled);
RegisterSetting("hidden", m_Hidden);
RegisterSetting("size", m_Size);
RegisterSetting("style", m_Style);
RegisterSetting("hotkey", m_Hotkey);
RegisterSetting("z", m_Z);
RegisterSetting("absolute", m_Absolute);
RegisterSetting("ghost", m_Ghost);
RegisterSetting("aspectratio", m_AspectRatio);
RegisterSetting("tooltip", m_Tooltip);
RegisterSetting("tooltip_style", m_TooltipStyle);
// Setup important defaults
// TODO: Should be in the default style?
SetSetting("hidden", false, true);
SetSetting("ghost", false, true);
SetSetting("enabled", true, true);
SetSetting("absolute", true, true);
}
IGUIObject::~IGUIObject()
{
for (const std::pair& p : m_Settings)
delete p.second;
if (!m_ScriptHandlers.empty())
JS_RemoveExtraGCRootsTracer(m_pGUI.GetScriptInterface()->GetJSRuntime(), Trace, this);
// m_Children is deleted along all other GUI Objects in the CGUI destructor
}
void IGUIObject::AddChild(IGUIObject& pChild)
{
pChild.SetParent(this);
m_Children.push_back(&pChild);
}
template
void IGUIObject::RegisterSetting(const CStr& Name, T& Value)
{
if (SettingExists(Name))
LOGERROR("The setting '%s' already exists on the object '%s'!", Name.c_str(), GetPresentableName().c_str());
else
m_Settings.emplace(Name, new CGUISetting(*this, Name, Value));
}
bool IGUIObject::SettingExists(const CStr& Setting) const
{
return m_Settings.find(Setting) != m_Settings.end();
}
template
T& IGUIObject::GetSetting(const CStr& Setting)
{
return static_cast* >(m_Settings.at(Setting))->m_pSetting;
}
template
const T& IGUIObject::GetSetting(const CStr& Setting) const
{
return static_cast* >(m_Settings.at(Setting))->m_pSetting;
}
bool IGUIObject::SetSettingFromString(const CStr& Setting, const CStrW& Value, const bool SendMessage)
{
const std::map::iterator it = m_Settings.find(Setting);
if (it == m_Settings.end())
{
LOGERROR("GUI object '%s' has no property called '%s', can't set parse and set value '%s'", GetPresentableName().c_str(), Setting.c_str(), Value.ToUTF8().c_str());
return false;
}
return it->second->FromString(Value, SendMessage);
}
template
void IGUIObject::SetSetting(const CStr& Setting, T& Value, const bool SendMessage)
{
PreSettingChange(Setting);
static_cast* >(m_Settings.at(Setting))->m_pSetting = std::move(Value);
SettingChanged(Setting, SendMessage);
}
template
void IGUIObject::SetSetting(const CStr& Setting, const T& Value, const bool SendMessage)
{
PreSettingChange(Setting);
static_cast* >(m_Settings.at(Setting))->m_pSetting = Value;
SettingChanged(Setting, SendMessage);
}
void IGUIObject::PreSettingChange(const CStr& Setting)
{
if (Setting == "hotkey")
m_pGUI.UnsetObjectHotkey(this, GetSetting(Setting));
}
void IGUIObject::SettingChanged(const CStr& Setting, const bool SendMessage)
{
if (Setting == "size")
{
// If setting was "size", we need to re-cache itself and all children
RecurseObject(nullptr, &IGUIObject::UpdateCachedSize);
}
else if (Setting == "hidden")
{
// Hiding an object requires us to reset it and all children
if (m_Hidden)
RecurseObject(nullptr, &IGUIObject::ResetStates);
}
else if (Setting == "hotkey")
m_pGUI.SetObjectHotkey(this, GetSetting(Setting));
if (SendMessage)
{
SGUIMessage msg(GUIM_SETTINGS_UPDATED, Setting);
HandleMessage(msg);
}
}
bool IGUIObject::IsMouseOver() const
{
return m_CachedActualSize.PointInside(m_pGUI.GetMousePos());
}
bool IGUIObject::MouseOverIcon()
{
return false;
}
void IGUIObject::UpdateMouseOver(IGUIObject* const& pMouseOver)
{
if (pMouseOver == this)
{
if (!m_MouseHovering)
SendMouseEvent(GUIM_MOUSE_ENTER,EventNameMouseEnter);
m_MouseHovering = true;
SendMouseEvent(GUIM_MOUSE_OVER, EventNameMouseMove);
}
else
{
if (m_MouseHovering)
{
m_MouseHovering = false;
SendMouseEvent(GUIM_MOUSE_LEAVE, EventNameMouseLeave);
}
}
}
void IGUIObject::ChooseMouseOverAndClosest(IGUIObject*& pObject)
{
if (!IsMouseOver())
return;
// Check if we've got competition at all
if (pObject == nullptr)
{
pObject = this;
return;
}
// Or if it's closer
if (GetBufferedZ() >= pObject->GetBufferedZ())
{
pObject = this;
return;
}
}
IGUIObject* IGUIObject::GetParent() const
{
// Important, we're not using GetParent() for these
// checks, that could screw it up
if (m_pParent && m_pParent->m_pParent == nullptr)
return nullptr;
return m_pParent;
}
void IGUIObject::ResetStates()
{
// Notify the gui that we aren't hovered anymore
UpdateMouseOver(nullptr);
}
void IGUIObject::UpdateCachedSize()
{
// If absolute="false" and the object has got a parent,
// use its cached size instead of the screen. Notice
// it must have just been cached for it to work.
if (!m_Absolute && m_pParent && !IsRootObject())
m_CachedActualSize = m_Size.GetSize(m_pParent->m_CachedActualSize);
else
m_CachedActualSize = m_Size.GetSize(CRect(0.f, 0.f, g_xres / g_GuiScale, g_yres / g_GuiScale));
// In a few cases, GUI objects have to resize to fill the screen
// but maintain a constant aspect ratio.
// Adjust the size to be the max possible, centered in the original size:
if (m_AspectRatio)
{
if (m_CachedActualSize.GetWidth() > m_CachedActualSize.GetHeight() * m_AspectRatio)
{
float delta = m_CachedActualSize.GetWidth() - m_CachedActualSize.GetHeight() * m_AspectRatio;
m_CachedActualSize.left += delta/2.f;
m_CachedActualSize.right -= delta/2.f;
}
else
{
float delta = m_CachedActualSize.GetHeight() - m_CachedActualSize.GetWidth() / m_AspectRatio;
m_CachedActualSize.bottom -= delta/2.f;
m_CachedActualSize.top += delta/2.f;
}
}
}
void IGUIObject::LoadStyle(const CStr& StyleName)
{
if (!m_pGUI.HasStyle(StyleName))
debug_warn(L"IGUIObject::LoadStyle failed");
// The default style may specify settings for any GUI object.
// Other styles are reported if they specify a Setting that does not exist,
// so that the XML author is informed and can correct the style.
for (const std::pair& p : m_pGUI.GetStyle(StyleName).m_SettingsDefaults)
{
if (SettingExists(p.first))
SetSettingFromString(p.first, p.second, true);
else if (StyleName != "default")
LOGWARNING("GUI object has no setting \"%s\", but the style \"%s\" defines it", p.first, StyleName.c_str());
}
}
float IGUIObject::GetBufferedZ() const
{
if (m_Absolute)
return m_Z;
if (GetParent())
return GetParent()->GetBufferedZ() + m_Z;
// In philosophy, a parentless object shouldn't be able to have a relative sizing,
// but we'll accept it so that absolute can be used as default without a complaint.
// Also, you could consider those objects children to the screen resolution.
return m_Z;
}
void IGUIObject::RegisterScriptHandler(const CStr& eventName, const CStr& Code, CGUI& pGUI)
{
JSContext* cx = pGUI.GetScriptInterface()->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue globalVal(cx, pGUI.GetGlobalObject());
JS::RootedObject globalObj(cx, &globalVal.toObject());
const int paramCount = 1;
const char* paramNames[paramCount] = { "mouse" };
// Location to report errors from
CStr CodeName = GetName() + " " + eventName;
// Generate a unique name
static int x = 0;
char buf[64];
sprintf_s(buf, ARRAY_SIZE(buf), "__eventhandler%d (%s)", x++, eventName.c_str());
JS::CompileOptions options(cx);
options.setFileAndLine(CodeName.c_str(), 0);
options.setIsRunOnce(false);
JS::RootedFunction func(cx);
JS::AutoObjectVector emptyScopeChain(cx);
if (!JS::CompileFunction(cx, emptyScopeChain, options, buf, paramCount, paramNames, Code.c_str(), Code.length(), &func))
{
LOGERROR("RegisterScriptHandler: Failed to compile the script for %s", eventName.c_str());
return;
}
JS::RootedObject funcObj(cx, JS_GetFunctionObject(func));
SetScriptHandler(eventName, funcObj);
}
void IGUIObject::SetScriptHandler(const CStr& eventName, JS::HandleObject Function)
{
if (m_ScriptHandlers.empty())
JS_AddExtraGCRootsTracer(m_pGUI.GetScriptInterface()->GetJSRuntime(), Trace, this);
m_ScriptHandlers[eventName] = JS::Heap(Function);
}
void IGUIObject::UnsetScriptHandler(const CStr& eventName)
{
std::map >::iterator it = m_ScriptHandlers.find(eventName);
if (it == m_ScriptHandlers.end())
return;
m_ScriptHandlers.erase(it);
if (m_ScriptHandlers.empty())
JS_RemoveExtraGCRootsTracer(m_pGUI.GetScriptInterface()->GetJSRuntime(), Trace, this);
}
InReaction IGUIObject::SendEvent(EGUIMessageType type, const CStr& eventName)
{
PROFILE2_EVENT("gui event");
PROFILE2_ATTR("type: %s", eventName.c_str());
PROFILE2_ATTR("object: %s", m_Name.c_str());
SGUIMessage msg(type);
HandleMessage(msg);
ScriptEvent(eventName);
return msg.skipped ? IN_PASS : IN_HANDLED;
}
InReaction IGUIObject::SendMouseEvent(EGUIMessageType type, const CStr& eventName)
{
PROFILE2_EVENT("gui mouse event");
PROFILE2_ATTR("type: %s", eventName.c_str());
PROFILE2_ATTR("object: %s", m_Name.c_str());
SGUIMessage msg(type);
HandleMessage(msg);
JSContext* cx = m_pGUI.GetScriptInterface()->GetContext();
JSAutoRequest rq(cx);
// Set up the 'mouse' parameter
JS::RootedValue mouse(cx);
const CPos& mousePos = m_pGUI.GetMousePos();
ScriptInterface::CreateObject(
cx,
&mouse,
"x", mousePos.x,
"y", mousePos.y,
"buttons", m_pGUI.GetMouseButtons());
JS::AutoValueVector paramData(cx);
paramData.append(mouse);
ScriptEvent(eventName, paramData);
return msg.skipped ? IN_PASS : IN_HANDLED;
}
void IGUIObject::ScriptEvent(const CStr& eventName)
{
ScriptEventWithReturn(eventName);
}
bool IGUIObject::ScriptEventWithReturn(const CStr& eventName)
{
if (m_ScriptHandlers.find(eventName) == m_ScriptHandlers.end())
return false;
JSContext* cx = m_pGUI.GetScriptInterface()->GetContext();
JSAutoRequest rq(cx);
JS::AutoValueVector paramData(cx);
return ScriptEventWithReturn(eventName, paramData);
}
void IGUIObject::ScriptEvent(const CStr& eventName, const JS::HandleValueArray& paramData)
{
ScriptEventWithReturn(eventName, paramData);
}
bool IGUIObject::ScriptEventWithReturn(const CStr& eventName, const JS::HandleValueArray& paramData)
{
std::map >::iterator it = m_ScriptHandlers.find(eventName);
if (it == m_ScriptHandlers.end())
return false;
JSContext* cx = m_pGUI.GetScriptInterface()->GetContext();
JSAutoRequest rq(cx);
JS::RootedObject obj(cx, GetJSObject());
JS::RootedValue handlerVal(cx, JS::ObjectValue(*it->second));
JS::RootedValue result(cx);
if (!JS_CallFunctionValue(cx, obj, handlerVal, paramData, &result))
{
LOGERROR("Errors executing script event \"%s\"", eventName.c_str());
return false;
}
return JS::ToBoolean(result);
}
void IGUIObject::CreateJSObject()
{
JSContext* cx = m_pGUI.GetScriptInterface()->GetContext();
JSAutoRequest rq(cx);
m_JSObject.init(cx, m_pGUI.GetScriptInterface()->CreateCustomObject("GUIObject"));
JS_SetPrivate(m_JSObject.get(), this);
RegisterScriptFunctions();
}
JSObject* IGUIObject::GetJSObject()
{
// Cache the object when somebody first asks for it, because otherwise
// we end up doing far too much object allocation.
if (!m_JSObject.initialized())
CreateJSObject();
return m_JSObject.get();
}
bool IGUIObject::IsEnabled() const
{
return m_Enabled;
}
bool IGUIObject::IsHidden() const
{
return m_Hidden;
}
bool IGUIObject::IsHiddenOrGhost() const
{
return m_Hidden || m_Ghost;
}
void IGUIObject::PlaySound(const CStrW& soundPath) const
{
if (g_SoundManager && !soundPath.empty())
g_SoundManager->PlayAsUI(soundPath.c_str(), false);
}
CStr IGUIObject::GetPresentableName() const
{
// __internal(), must be at least 13 letters to be able to be
// an internal name
if (m_Name.length() <= 12)
return m_Name;
if (m_Name.substr(0, 10) == "__internal")
return CStr("[unnamed object]");
else
return m_Name;
}
void IGUIObject::SetFocus()
{
m_pGUI.SetFocusedObject(this);
}
bool IGUIObject::IsFocused() const
{
return m_pGUI.GetFocusedObject() == this;
}
bool IGUIObject::IsBaseObject() const
{
return this == &m_pGUI.GetBaseObject();
}
bool IGUIObject::IsRootObject() const
{
return m_pParent == &m_pGUI.GetBaseObject();
}
void IGUIObject::TraceMember(JSTracer* trc)
{
// Please ensure to adapt the Tracer enabling and disabling in accordance with the GC things traced!
for (std::pair>& handler : m_ScriptHandlers)
- JS_CallObjectTracer(trc, &handler.second, "IGUIObject::m_ScriptHandlers");
+ JS::TraceEdge(trc, &handler.second, "IGUIObject::m_ScriptHandlers");
}
// Instantiate templated functions:
// These functions avoid copies by working with a reference and move semantics.
#define TYPE(T) \
template void IGUIObject::RegisterSetting(const CStr& Name, T& Value); \
template T& IGUIObject::GetSetting(const CStr& Setting); \
template const T& IGUIObject::GetSetting(const CStr& Setting) const; \
template void IGUIObject::SetSetting(const CStr& Setting, T& Value, const bool SendMessage); \
#include "gui/GUISettingTypes.h"
#undef TYPE
// Copying functions - discouraged except for primitives.
#define TYPE(T) \
template void IGUIObject::SetSetting(const CStr& Setting, const T& Value, const bool SendMessage); \
#define GUITYPE_IGNORE_NONCOPYABLE
#include "gui/GUISettingTypes.h"
#undef GUITYPE_IGNORE_NONCOPYABLE
#undef TYPE
Index: ps/trunk/source/lobby/XmppClient.cpp
===================================================================
--- ps/trunk/source/lobby/XmppClient.cpp (revision 24168)
+++ ps/trunk/source/lobby/XmppClient.cpp (revision 24169)
@@ -1,1384 +1,1384 @@
/* Copyright (C) 2020 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "XmppClient.h"
#include "StanzaExtensions.h"
#ifdef WIN32
# include
#endif
#include "i18n/L10n.h"
#include "lib/external_libraries/enet.h"
#include "lib/utf8.h"
#include "network/NetServer.h"
#include "network/StunClient.h"
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
#include "ps/Pyrogenesis.h"
#include "scriptinterface/ScriptInterface.h"
#include
//debug
#if 1
#define DbgXMPP(x)
#else
#define DbgXMPP(x) std::cout << x << std::endl;
static std::string tag_xml(const glooxwrapper::IQ& iq)
{
std::string ret;
glooxwrapper::Tag* tag = iq.tag();
ret = tag->xml().to_string();
glooxwrapper::Tag::free(tag);
return ret;
}
#endif
static std::string tag_name(const glooxwrapper::IQ& iq)
{
std::string ret;
glooxwrapper::Tag* tag = iq.tag();
ret = tag->name().to_string();
glooxwrapper::Tag::free(tag);
return ret;
}
IXmppClient* IXmppClient::create(const ScriptInterface* scriptInterface, const std::string& sUsername, const std::string& sPassword, const std::string& sRoom, const std::string& sNick, const int historyRequestSize,bool regOpt)
{
return new XmppClient(scriptInterface, sUsername, sPassword, sRoom, sNick, historyRequestSize, regOpt);
}
/**
* Construct the XMPP client.
*
* @param scriptInterface - ScriptInterface to be used for storing GUI messages.
* Can be left blank for non-visual applications.
* @param sUsername Username to login with of register.
* @param sPassword Password to login with or register.
* @param sRoom MUC room to join.
* @param sNick Nick to join with.
* @param historyRequestSize Number of stanzas of room history to request.
* @param regOpt If we are just registering or not.
*/
XmppClient::XmppClient(const ScriptInterface* scriptInterface, const std::string& sUsername, const std::string& sPassword, const std::string& sRoom, const std::string& sNick, const int historyRequestSize, bool regOpt)
: m_ScriptInterface(scriptInterface),
m_client(nullptr),
m_mucRoom(nullptr),
m_registration(nullptr),
m_username(sUsername),
m_password(sPassword),
m_room(sRoom),
m_nick(sNick),
m_initialLoadComplete(false),
m_isConnected(false),
m_sessionManager(nullptr),
m_certStatus(gloox::CertStatus::CertOk),
m_PlayerMapUpdate(false)
{
if (m_ScriptInterface)
JS_AddExtraGCRootsTracer(m_ScriptInterface->GetJSRuntime(), XmppClient::Trace, this);
// Read lobby configuration from default.cfg
std::string sXpartamupp;
std::string sEchelon;
CFG_GET_VAL("lobby.server", m_server);
CFG_GET_VAL("lobby.xpartamupp", sXpartamupp);
CFG_GET_VAL("lobby.echelon", sEchelon);
m_xpartamuppId = sXpartamupp + "@" + m_server + "/CC";
m_echelonId = sEchelon + "@" + m_server + "/CC";
glooxwrapper::JID clientJid(sUsername + "@" + m_server + "/0ad");
glooxwrapper::JID roomJid(m_room + "@conference." + m_server + "/" + sNick);
// If we are connecting, use the full jid and a password
// If we are registering, only use the server name
if (!regOpt)
m_client = new glooxwrapper::Client(clientJid, sPassword);
else
m_client = new glooxwrapper::Client(m_server);
// Optionally join without a TLS certificate, so a local server can be tested quickly.
// Security risks from malicious JS mods can be mitigated if this option and also the hostname and login are shielded from JS access.
bool tls = true;
CFG_GET_VAL("lobby.tls", tls);
m_client->setTls(tls ? gloox::TLSRequired : gloox::TLSDisabled);
// Disable use of the SASL PLAIN mechanism, to prevent leaking credentials
// if the server doesn't list any supported SASL mechanism or the response
// has been modified to exclude those.
const int mechs = gloox::SaslMechAll ^ gloox::SaslMechPlain;
m_client->setSASLMechanisms(mechs);
m_client->registerConnectionListener(this);
m_client->setPresence(gloox::Presence::Available, -1);
m_client->disco()->setVersion("Pyrogenesis", engine_version);
m_client->disco()->setIdentity("client", "bot");
m_client->setCompression(false);
m_client->registerStanzaExtension(new GameListQuery());
m_client->registerIqHandler(this, EXTGAMELISTQUERY);
m_client->registerStanzaExtension(new BoardListQuery());
m_client->registerIqHandler(this, EXTBOARDLISTQUERY);
m_client->registerStanzaExtension(new ProfileQuery());
m_client->registerIqHandler(this, EXTPROFILEQUERY);
m_client->registerStanzaExtension(new LobbyAuth());
m_client->registerIqHandler(this, EXTLOBBYAUTH);
m_client->registerMessageHandler(this);
// Uncomment to see the raw stanzas
//m_client->getWrapped()->logInstance().registerLogHandler( gloox::LogLevelDebug, gloox::LogAreaAll, this );
if (!regOpt)
{
// Create a Multi User Chat Room
m_mucRoom = new glooxwrapper::MUCRoom(m_client, roomJid, this, 0);
// Get room history.
m_mucRoom->setRequestHistory(historyRequestSize, gloox::MUCRoom::HistoryMaxStanzas);
}
else
{
// Registration
m_registration = new glooxwrapper::Registration(m_client);
m_registration->registerRegistrationHandler(this);
}
m_sessionManager = new glooxwrapper::SessionManager(m_client, this);
// Register plugins to allow gloox parse them in incoming sessions
m_sessionManager->registerPlugins();
}
/**
* Destroy the xmpp client
*/
XmppClient::~XmppClient()
{
DbgXMPP("XmppClient destroyed");
delete m_registration;
delete m_mucRoom;
delete m_sessionManager;
// Workaround for memory leak in gloox 1.0/1.0.1
m_client->removePresenceExtension(gloox::ExtCaps);
delete m_client;
for (const glooxwrapper::Tag* const& t : m_GameList)
glooxwrapper::Tag::free(t);
for (const glooxwrapper::Tag* const& t : m_BoardList)
glooxwrapper::Tag::free(t);
for (const glooxwrapper::Tag* const& t : m_Profile)
glooxwrapper::Tag::free(t);
if (m_ScriptInterface)
JS_RemoveExtraGCRootsTracer(m_ScriptInterface->GetJSRuntime(), XmppClient::Trace, this);
}
void XmppClient::TraceMember(JSTracer* trc)
{
for (JS::Heap& guiMessage : m_GuiMessageQueue)
- JS_CallValueTracer(trc, &guiMessage, "m_GuiMessageQueue");
+ JS::TraceEdge(trc, &guiMessage, "m_GuiMessageQueue");
for (JS::Heap& guiMessage : m_HistoricGuiMessages)
- JS_CallValueTracer(trc, &guiMessage, "m_HistoricGuiMessages");
+ JS::TraceEdge(trc, &guiMessage, "m_HistoricGuiMessages");
}
/// Network
void XmppClient::connect()
{
m_initialLoadComplete = false;
m_client->connect(false);
}
void XmppClient::disconnect()
{
m_client->disconnect();
}
bool XmppClient::isConnected()
{
return m_isConnected;
}
void XmppClient::recv()
{
m_client->recv(1);
}
/**
* Log (debug) Handler
*/
void XmppClient::handleLog(gloox::LogLevel level, gloox::LogArea area, const std::string& message)
{
std::cout << "log: level: " << level << ", area: " << area << ", message: " << message << std::endl;
}
/*****************************************************
* Connection handlers *
*****************************************************/
/**
* Handle connection
*/
void XmppClient::onConnect()
{
if (m_mucRoom)
{
m_isConnected = true;
CreateGUIMessage("system", "connected", std::time(nullptr));
m_mucRoom->join();
}
if (m_registration)
m_registration->fetchRegistrationFields();
}
/**
* Handle disconnection
*/
void XmppClient::onDisconnect(gloox::ConnectionError error)
{
// Make sure we properly leave the room so that
// everything works if we decide to come back later
if (m_mucRoom)
m_mucRoom->leave();
// Clear game, board and player lists.
for (const glooxwrapper::Tag* const& t : m_GameList)
glooxwrapper::Tag::free(t);
for (const glooxwrapper::Tag* const& t : m_BoardList)
glooxwrapper::Tag::free(t);
for (const glooxwrapper::Tag* const& t : m_Profile)
glooxwrapper::Tag::free(t);
m_BoardList.clear();
m_GameList.clear();
m_PlayerMap.clear();
m_PlayerMapUpdate = true;
m_Profile.clear();
m_HistoricGuiMessages.clear();
m_isConnected = false;
m_initialLoadComplete = false;
CreateGUIMessage(
"system",
"disconnected",
std::time(nullptr),
"reason", error,
"certificate_status", m_certStatus);
}
/**
* Handle TLS connection.
*/
bool XmppClient::onTLSConnect(const glooxwrapper::CertInfo& info)
{
DbgXMPP("onTLSConnect");
DbgXMPP(
"status: " << info.status <<
"\nissuer: " << info.issuer <<
"\npeer: " << info.server <<
"\nprotocol: " << info.protocol <<
"\nmac: " << info.mac <<
"\ncipher: " << info.cipher <<
"\ncompression: " << info.compression );
m_certStatus = static_cast(info.status);
// Optionally accept invalid certificates, see require_tls option.
bool verify_certificate = true;
CFG_GET_VAL("lobby.verify_certificate", verify_certificate);
return info.status == gloox::CertOk || !verify_certificate;
}
/**
* Handle MUC room errors
*/
void XmppClient::handleMUCError(glooxwrapper::MUCRoom& UNUSED(room), gloox::StanzaError err)
{
DbgXMPP("MUC Error " << ": " << StanzaErrorToString(err));
CreateGUIMessage("system", "error", std::time(nullptr), "text", err);
}
/*****************************************************
* Requests to server *
*****************************************************/
/**
* Request the leaderboard data from the server.
*/
void XmppClient::SendIqGetBoardList()
{
glooxwrapper::JID echelonJid(m_echelonId);
// Send IQ
BoardListQuery* b = new BoardListQuery();
b->m_Command = "getleaderboard";
glooxwrapper::IQ iq(gloox::IQ::Get, echelonJid, m_client->getID());
iq.addExtension(b);
DbgXMPP("SendIqGetBoardList [" << tag_xml(iq) << "]");
m_client->send(iq);
}
/**
* Request the profile data from the server.
*/
void XmppClient::SendIqGetProfile(const std::string& player)
{
glooxwrapper::JID echelonJid(m_echelonId);
// Send IQ
ProfileQuery* b = new ProfileQuery();
b->m_Command = player;
glooxwrapper::IQ iq(gloox::IQ::Get, echelonJid, m_client->getID());
iq.addExtension(b);
DbgXMPP("SendIqGetProfile [" << tag_xml(iq) << "]");
m_client->send(iq);
}
/**
* Send game report containing numerous game properties to the server.
*
* @param data A JS array of game statistics
*/
void XmppClient::SendIqGameReport(const ScriptInterface& scriptInterface, JS::HandleValue data)
{
glooxwrapper::JID echelonJid(m_echelonId);
// Setup some base stanza attributes
GameReport* game = new GameReport();
glooxwrapper::Tag* report = glooxwrapper::Tag::allocate("game");
// Iterate through all the properties reported and add them to the stanza.
std::vector properties;
scriptInterface.EnumeratePropertyNames(data, true, properties);
for (const std::string& p : properties)
{
std::wstring value;
scriptInterface.GetProperty(data, p.c_str(), value);
report->addAttribute(p, utf8_from_wstring(value));
}
// Add stanza to IQ
game->m_GameReport.emplace_back(report);
// Send IQ
glooxwrapper::IQ iq(gloox::IQ::Set, echelonJid, m_client->getID());
iq.addExtension(game);
DbgXMPP("SendGameReport [" << tag_xml(iq) << "]");
m_client->send(iq);
};
/**
* Send a request to register a game to the server.
*
* @param data A JS array of game attributes
*/
void XmppClient::SendIqRegisterGame(const ScriptInterface& scriptInterface, JS::HandleValue data)
{
glooxwrapper::JID xpartamuppJid(m_xpartamuppId);
// Setup some base stanza attributes
GameListQuery* g = new GameListQuery();
g->m_Command = "register";
glooxwrapper::Tag* game = glooxwrapper::Tag::allocate("game");
// Add a fake ip which will be overwritten by the ip stamp XMPP module on the server.
game->addAttribute("ip", "fake");
// Iterate through all the properties reported and add them to the stanza.
std::vector properties;
scriptInterface.EnumeratePropertyNames(data, true, properties);
for (const std::string& p : properties)
{
std::wstring value;
scriptInterface.GetProperty(data, p.c_str(), value);
game->addAttribute(p, utf8_from_wstring(value));
}
// Push the stanza onto the IQ
g->m_GameList.emplace_back(game);
// Send IQ
glooxwrapper::IQ iq(gloox::IQ::Set, xpartamuppJid, m_client->getID());
iq.addExtension(g);
DbgXMPP("SendIqRegisterGame [" << tag_xml(iq) << "]");
m_client->send(iq);
}
/**
* Send a request to unregister a game to the server.
*/
void XmppClient::SendIqUnregisterGame()
{
glooxwrapper::JID xpartamuppJid(m_xpartamuppId);
// Send IQ
GameListQuery* g = new GameListQuery();
g->m_Command = "unregister";
g->m_GameList.emplace_back(glooxwrapper::Tag::allocate("game"));
glooxwrapper::IQ iq(gloox::IQ::Set, xpartamuppJid, m_client->getID());
iq.addExtension(g);
DbgXMPP("SendIqUnregisterGame [" << tag_xml(iq) << "]");
m_client->send(iq);
}
/**
* Send a request to change the state of a registered game on the server.
*
* A game can either be in the 'running' or 'waiting' state - the server
* decides which - but we need to update the current players that are
* in-game so the server can make the calculation.
*/
void XmppClient::SendIqChangeStateGame(const std::string& nbp, const std::string& players)
{
glooxwrapper::JID xpartamuppJid(m_xpartamuppId);
// Send IQ
GameListQuery* g = new GameListQuery();
g->m_Command = "changestate";
glooxwrapper::Tag* game = glooxwrapper::Tag::allocate("game");
game->addAttribute("nbp", nbp);
game->addAttribute("players", players);
g->m_GameList.emplace_back(game);
glooxwrapper::IQ iq(gloox::IQ::Set, xpartamuppJid, m_client->getID());
iq.addExtension(g);
DbgXMPP("SendIqChangeStateGame [" << tag_xml(iq) << "]");
m_client->send(iq);
}
/*****************************************************
* iq to clients *
*****************************************************/
/**
* Send lobby authentication token.
*/
void XmppClient::SendIqLobbyAuth(const std::string& to, const std::string& token)
{
LobbyAuth* auth = new LobbyAuth();
auth->m_Token = token;
glooxwrapper::JID clientJid(to + "@" + m_server + "/0ad");
glooxwrapper::IQ iq(gloox::IQ::Set, clientJid, m_client->getID());
iq.addExtension(auth);
DbgXMPP("SendIqLobbyAuth [" << tag_xml(iq) << "]");
m_client->send(iq);
}
/*****************************************************
* Account registration *
*****************************************************/
void XmppClient::handleRegistrationFields(const glooxwrapper::JID&, int fields, glooxwrapper::string)
{
glooxwrapper::RegistrationFields vals;
vals.username = m_username;
vals.password = m_password;
m_registration->createAccount(fields, vals);
}
void XmppClient::handleRegistrationResult(const glooxwrapper::JID&, gloox::RegistrationResult result)
{
if (result == gloox::RegistrationSuccess)
CreateGUIMessage("system", "registered", std::time(nullptr));
else
CreateGUIMessage("system", "error", std::time(nullptr), "text", result);
disconnect();
}
void XmppClient::handleAlreadyRegistered(const glooxwrapper::JID&)
{
DbgXMPP("the account already exists");
}
void XmppClient::handleDataForm(const glooxwrapper::JID&, const glooxwrapper::DataForm&)
{
DbgXMPP("dataForm received");
}
void XmppClient::handleOOB(const glooxwrapper::JID&, const glooxwrapper::OOB&)
{
DbgXMPP("OOB registration requested");
}
/*****************************************************
* Requests from GUI *
*****************************************************/
/**
* Handle requests from the GUI for the list of players.
*
* @return A JS array containing all known players and their presences
*/
void XmppClient::GUIGetPlayerList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret)
{
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
ScriptInterface::CreateArray(cx, ret);
int j = 0;
for (const std::pair& p : m_PlayerMap)
{
JS::RootedValue player(cx);
ScriptInterface::CreateObject(
cx,
&player,
"name", p.first,
"presence", p.second.m_Presence,
"rating", p.second.m_Rating,
"role", p.second.m_Role);
scriptInterface.SetPropertyInt(ret, j++, player);
}
}
/**
* Handle requests from the GUI for the list of all active games.
*
* @return A JS array containing all known games
*/
void XmppClient::GUIGetGameList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret)
{
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
ScriptInterface::CreateArray(cx, ret);
int j = 0;
const char* stats[] = { "name", "ip", "port", "stunIP", "stunPort", "hostUsername", "state",
"nbp", "maxnbp", "players", "mapName", "niceMapName", "mapSize", "mapType",
"victoryConditions", "startTime", "mods" };
for(const glooxwrapper::Tag* const& t : m_GameList)
{
JS::RootedValue game(cx);
ScriptInterface::CreateObject(cx, &game);
for (size_t i = 0; i < ARRAY_SIZE(stats); ++i)
scriptInterface.SetProperty(game, stats[i], t->findAttribute(stats[i]));
scriptInterface.SetPropertyInt(ret, j++, game);
}
}
/**
* Handle requests from the GUI for leaderboard data.
*
* @return A JS array containing all known leaderboard data
*/
void XmppClient::GUIGetBoardList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret)
{
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
ScriptInterface::CreateArray(cx, ret);
int j = 0;
const char* attributes[] = { "name", "rank", "rating" };
for(const glooxwrapper::Tag* const& t : m_BoardList)
{
JS::RootedValue board(cx);
ScriptInterface::CreateObject(cx, &board);
for (size_t i = 0; i < ARRAY_SIZE(attributes); ++i)
scriptInterface.SetProperty(board, attributes[i], t->findAttribute(attributes[i]));
scriptInterface.SetPropertyInt(ret, j++, board);
}
}
/**
* Handle requests from the GUI for profile data.
*
* @return A JS array containing the specific user's profile data
*/
void XmppClient::GUIGetProfile(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret)
{
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
ScriptInterface::CreateArray(cx, ret);
int j = 0;
const char* stats[] = { "player", "rating", "totalGamesPlayed", "highestRating", "wins", "losses", "rank" };
for (const glooxwrapper::Tag* const& t : m_Profile)
{
JS::RootedValue profile(cx);
ScriptInterface::CreateObject(cx, &profile);
for (size_t i = 0; i < ARRAY_SIZE(stats); ++i)
scriptInterface.SetProperty(profile, stats[i], t->findAttribute(stats[i]));
scriptInterface.SetPropertyInt(ret, j++, profile);
}
}
/*****************************************************
* Message interfaces *
*****************************************************/
void SetGUIMessageProperty(JSContext* UNUSED(cx), JS::HandleObject UNUSED(messageObj))
{
}
template
void SetGUIMessageProperty(JSContext* cx, JS::HandleObject messageObj, const std::string& propertyName, const T& propertyValue, Args const&... args)
{
// JSAutoRequest is the responsibility of the caller
JS::RootedValue scriptPropertyValue(cx);
ScriptInterface::AssignOrToJSVal(cx, &scriptPropertyValue, propertyValue);
JS_DefineProperty(cx, messageObj, propertyName.c_str(), scriptPropertyValue, JSPROP_ENUMERATE);
SetGUIMessageProperty(cx, messageObj, args...);
}
template
void XmppClient::CreateGUIMessage(
const std::string& type,
const std::string& level,
const std::time_t time,
Args const&... args)
{
if (!m_ScriptInterface)
return;
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue message(cx);
ScriptInterface::CreateObject(
cx,
&message,
"type", type,
"level", level,
"historic", false,
"time", static_cast(time));
JS::RootedObject messageObj(cx, message.toObjectOrNull());
SetGUIMessageProperty(cx, messageObj, args...);
m_ScriptInterface->FreezeObject(message, true);
m_GuiMessageQueue.push_back(JS::Heap(message));
}
bool XmppClient::GuiPollHasPlayerListUpdate()
{
// The initial playerlist will be received in multiple messages
// Only inform the GUI after all of these playerlist fragments were received.
if (!m_initialLoadComplete)
return false;
bool hasUpdate = m_PlayerMapUpdate;
m_PlayerMapUpdate = false;
return hasUpdate;
}
JS::Value XmppClient::GuiPollNewMessages(const ScriptInterface& scriptInterface)
{
if ((m_isConnected && !m_initialLoadComplete) || m_GuiMessageQueue.empty())
return JS::UndefinedValue();
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
// Optimize for batch message processing that is more
// performance demanding than processing a lone message.
JS::RootedValue messages(cx);
ScriptInterface::CreateArray(cx, &messages);
int j = 0;
for (const JS::Heap& message : m_GuiMessageQueue)
{
m_ScriptInterface->SetPropertyInt(messages, j++, message);
// Store historic chat messages.
// Only store relevant messages to minimize memory footprint.
JS::RootedValue rootedMessage(cx, message);
std::string type;
m_ScriptInterface->GetProperty(rootedMessage, "type", type);
if (type != "chat")
continue;
std::string level;
m_ScriptInterface->GetProperty(rootedMessage, "level", level);
if (level != "room-message" && level != "private-message")
continue;
JS::RootedValue historicMessage(cx);
if (JS_StructuredClone(cx, rootedMessage, &historicMessage, nullptr, nullptr))
{
m_ScriptInterface->SetProperty(historicMessage, "historic", true);
m_ScriptInterface->FreezeObject(historicMessage, true);
m_HistoricGuiMessages.push_back(JS::Heap(historicMessage));
}
else
LOGERROR("Could not clone historic lobby GUI message!");
}
m_GuiMessageQueue.clear();
// Copy the messages over to the caller script interface.
return scriptInterface.CloneValueFromOtherContext(*m_ScriptInterface, messages);
}
JS::Value XmppClient::GuiPollHistoricMessages(const ScriptInterface& scriptInterface)
{
if (m_HistoricGuiMessages.empty())
return JS::UndefinedValue();
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue messages(cx);
ScriptInterface::CreateArray(cx, &messages);
int j = 0;
for (const JS::Heap& message : m_HistoricGuiMessages)
m_ScriptInterface->SetPropertyInt(messages, j++, message);
// Copy the messages over to the caller script interface.
return scriptInterface.CloneValueFromOtherContext(*m_ScriptInterface, messages);
}
/**
* Send a standard MUC textual message.
*/
void XmppClient::SendMUCMessage(const std::string& message)
{
m_mucRoom->send(message);
}
/**
* Handle a room message.
*/
void XmppClient::handleMUCMessage(glooxwrapper::MUCRoom& UNUSED(room), const glooxwrapper::Message& msg, bool priv)
{
DbgXMPP(msg.from().resource() << " said " << msg.body());
CreateGUIMessage(
"chat",
priv ? "private-message" : "room-message",
ComputeTimestamp(msg),
"from", msg.from().resource(),
"text", msg.body());
}
/**
* Handle a private message.
*/
void XmppClient::handleMessage(const glooxwrapper::Message& msg, glooxwrapper::MessageSession*)
{
DbgXMPP("type " << msg.subtype() << ", subject " << msg.subject()
<< ", message " << msg.body() << ", thread id " << msg.thread());
CreateGUIMessage(
"chat",
"private-message",
ComputeTimestamp(msg),
"from", msg.from().resource(),
"text", msg.body());
}
/**
* Handle portions of messages containing custom stanza extensions.
*/
bool XmppClient::handleIq(const glooxwrapper::IQ& iq)
{
DbgXMPP("handleIq [" << tag_xml(iq) << "]");
if (iq.subtype() == gloox::IQ::Result)
{
const GameListQuery* gq = iq.findExtension(EXTGAMELISTQUERY);
const BoardListQuery* bq = iq.findExtension(EXTBOARDLISTQUERY);
const ProfileQuery* pq = iq.findExtension(EXTPROFILEQUERY);
if (gq)
{
for (const glooxwrapper::Tag* const& t : m_GameList)
glooxwrapper::Tag::free(t);
m_GameList.clear();
for (const glooxwrapper::Tag* const& t : gq->m_GameList)
m_GameList.emplace_back(t->clone());
CreateGUIMessage("game", "gamelist", std::time(nullptr));
}
if (bq)
{
if (bq->m_Command == "boardlist")
{
for (const glooxwrapper::Tag* const& t : m_BoardList)
glooxwrapper::Tag::free(t);
m_BoardList.clear();
for (const glooxwrapper::Tag* const& t : bq->m_StanzaBoardList)
m_BoardList.emplace_back(t->clone());
CreateGUIMessage("game", "leaderboard", std::time(nullptr));
}
else if (bq->m_Command == "ratinglist")
{
for (const glooxwrapper::Tag* const& t : bq->m_StanzaBoardList)
{
const PlayerMap::iterator it = m_PlayerMap.find(t->findAttribute("name"));
if (it != m_PlayerMap.end())
{
it->second.m_Rating = t->findAttribute("rating");
m_PlayerMapUpdate = true;
}
}
CreateGUIMessage("game", "ratinglist", std::time(nullptr));
}
}
if (pq)
{
for (const glooxwrapper::Tag* const& t : m_Profile)
glooxwrapper::Tag::free(t);
m_Profile.clear();
for (const glooxwrapper::Tag* const& t : pq->m_StanzaProfile)
m_Profile.emplace_back(t->clone());
CreateGUIMessage("game", "profile", std::time(nullptr));
}
}
else if (iq.subtype() == gloox::IQ::Set)
{
const LobbyAuth* lobbyAuth = iq.findExtension(EXTLOBBYAUTH);
if (lobbyAuth)
{
LOGMESSAGE("XmppClient: Received lobby auth: %s from %s", lobbyAuth->m_Token.to_string(), iq.from().username());
glooxwrapper::IQ response(gloox::IQ::Result, iq.from(), iq.id());
m_client->send(response);
if (g_NetServer)
g_NetServer->OnLobbyAuth(iq.from().username(), lobbyAuth->m_Token.to_string());
else
LOGERROR("Received lobby authentication request, but not hosting currently!");
}
}
else if (iq.subtype() == gloox::IQ::Error)
CreateGUIMessage("system", "error", std::time(nullptr), "text", iq.error_error());
else
{
CreateGUIMessage("system", "error", std::time(nullptr), "text", wstring_from_utf8(g_L10n.Translate("unknown subtype (see logs)")));
LOGMESSAGE("unknown subtype '%s'", tag_name(iq).c_str());
}
return true;
}
/**
* Update local data when a user changes presence.
*/
void XmppClient::handleMUCParticipantPresence(glooxwrapper::MUCRoom& UNUSED(room), const glooxwrapper::MUCRoomParticipant participant, const glooxwrapper::Presence& presence)
{
const glooxwrapper::string& nick = participant.nick->resource();
if (presence.presence() == gloox::Presence::Unavailable)
{
if (!participant.newNick.empty() && (participant.flags & (gloox::UserNickChanged | gloox::UserSelf)))
{
// we have a nick change
if (m_PlayerMap.find(participant.newNick) == m_PlayerMap.end())
m_PlayerMap.emplace(
std::piecewise_construct,
std::forward_as_tuple(participant.newNick),
std::forward_as_tuple(presence.presence(), participant.role, std::move(m_PlayerMap.at(nick).m_Rating)));
else
LOGERROR("Nickname changed to an existing nick!");
DbgXMPP(nick << " is now known as " << participant.newNick);
CreateGUIMessage(
"chat",
"nick",
std::time(nullptr),
"oldnick", nick,
"newnick", participant.newNick);
}
else if (participant.flags & gloox::UserKicked)
{
DbgXMPP(nick << " was kicked. Reason: " << participant.reason);
CreateGUIMessage(
"chat",
"kicked",
std::time(nullptr),
"nick", nick,
"reason", participant.reason);
}
else if (participant.flags & gloox::UserBanned)
{
DbgXMPP(nick << " was banned. Reason: " << participant.reason);
CreateGUIMessage(
"chat",
"banned",
std::time(nullptr),
"nick", nick,
"reason", participant.reason);
}
else
{
DbgXMPP(nick << " left the room (flags " << participant.flags << ")");
CreateGUIMessage(
"chat",
"leave",
std::time(nullptr),
"nick", nick);
}
m_PlayerMap.erase(nick);
}
else
{
const PlayerMap::iterator it = m_PlayerMap.find(nick);
/* During the initialization process, we receive join messages for everyone
* currently in the room. We don't want to display these, so we filter them
* out. We will always be the last to join during initialization.
*/
if (!m_initialLoadComplete)
{
if (m_mucRoom->nick() == nick)
m_initialLoadComplete = true;
}
else if (it == m_PlayerMap.end())
{
CreateGUIMessage(
"chat",
"join",
std::time(nullptr),
"nick", nick);
}
else if (it->second.m_Role != participant.role)
{
CreateGUIMessage(
"chat",
"role",
std::time(nullptr),
"nick", nick,
"oldrole", it->second.m_Role,
"newrole", participant.role);
}
else
{
// Don't create a GUI message for regular presence changes, because
// several hundreds of them accumulate during a match, impacting performance terribly and
// the only way they are used is to determine whether to update the playerlist.
}
DbgXMPP(
nick << " is in the room, "
"presence: " << GetPresenceString(presence.presence()) << ", "
"role: "<< GetRoleString(participant.role));
if (it == m_PlayerMap.end())
{
m_PlayerMap.emplace(
std::piecewise_construct,
std::forward_as_tuple(nick),
std::forward_as_tuple(presence.presence(), participant.role, std::string()));
}
else
{
it->second.m_Presence = presence.presence();
it->second.m_Role = participant.role;
}
}
m_PlayerMapUpdate = true;
}
/**
* Update local cache when subject changes.
*/
void XmppClient::handleMUCSubject(glooxwrapper::MUCRoom& UNUSED(room), const glooxwrapper::string& nick, const glooxwrapper::string& subject)
{
m_Subject = wstring_from_utf8(subject.to_string());
CreateGUIMessage(
"chat",
"subject",
std::time(nullptr),
"nick", nick,
"subject", m_Subject);
}
/**
* Get current subject.
*/
const std::wstring& XmppClient::GetSubject()
{
return m_Subject;
}
/**
* Request nick change, real change via mucRoomHandler.
*
* @param nick Desired nickname
*/
void XmppClient::SetNick(const std::string& nick)
{
m_mucRoom->setNick(nick);
}
/**
* Get current nickname.
*
* @param nick Variable to store the nickname in.
*/
void XmppClient::GetNick(std::string& nick)
{
nick = m_mucRoom->nick().to_string();
}
/**
* Kick a player from the current room.
*
* @param nick Nickname to be kicked
* @param reason Reason the player was kicked
*/
void XmppClient::kick(const std::string& nick, const std::string& reason)
{
m_mucRoom->kick(nick, reason);
}
/**
* Ban a player from the current room.
*
* @param nick Nickname to be banned
* @param reason Reason the player was banned
*/
void XmppClient::ban(const std::string& nick, const std::string& reason)
{
m_mucRoom->ban(nick, reason);
}
/**
* Change the xmpp presence of the client.
*
* @param presence A string containing the desired presence
*/
void XmppClient::SetPresence(const std::string& presence)
{
#define IF(x,y) if (presence == x) m_mucRoom->setPresence(gloox::Presence::y)
IF("available", Available);
else IF("chat", Chat);
else IF("away", Away);
else IF("playing", DND);
else IF("offline", Unavailable);
// The others are not to be set
#undef IF
else LOGERROR("Unknown presence '%s'", presence.c_str());
}
/**
* Get the current xmpp presence of the given nick.
*/
const char* XmppClient::GetPresence(const std::string& nick)
{
const PlayerMap::iterator it = m_PlayerMap.find(nick);
if (it == m_PlayerMap.end())
return "offline";
return GetPresenceString(it->second.m_Presence);
}
/**
* Get the current xmpp role of the given nick.
*/
const char* XmppClient::GetRole(const std::string& nick)
{
const PlayerMap::iterator it = m_PlayerMap.find(nick);
if (it == m_PlayerMap.end())
return "";
return GetRoleString(it->second.m_Role);
}
/**
* Get the most recent received rating of the given nick.
* Notice that this doesn't request a rating profile if it hasn't been received yet.
*/
std::wstring XmppClient::GetRating(const std::string& nick)
{
const PlayerMap::iterator it = m_PlayerMap.find(nick);
if (it == m_PlayerMap.end())
return std::wstring();
return wstring_from_utf8(it->second.m_Rating.to_string());
}
/*****************************************************
* Utilities *
*****************************************************/
/**
* Parse and return the timestamp of a historic chat message and return the current time for new chat messages.
* Historic chat messages are implement as DelayedDelivers as specified in XEP-0203.
* Hence, their timestamp MUST be in UTC and conform to the DateTime format XEP-0082.
*
* @returns Seconds since the epoch.
*/
std::time_t XmppClient::ComputeTimestamp(const glooxwrapper::Message& msg)
{
// Only historic messages contain a timestamp!
if (!msg.when())
return std::time(nullptr);
// The locale is irrelevant, because the XMPP date format doesn't contain written month names
for (const std::string& format : std::vector{ "Y-M-d'T'H:m:sZ", "Y-M-d'T'H:m:s.SZ" })
{
UDate dateTime = g_L10n.ParseDateTime(msg.when()->stamp().to_string(), format, icu::Locale::getUS());
if (dateTime)
return dateTime / 1000.0;
}
return std::time(nullptr);
}
/**
* Convert a gloox presence type to an untranslated string literal to be used as an identifier by the scripts.
*/
const char* XmppClient::GetPresenceString(const gloox::Presence::PresenceType presenceType)
{
switch (presenceType)
{
#define CASE(X,Y) case gloox::Presence::X: return Y
CASE(Available, "available");
CASE(Chat, "chat");
CASE(Away, "away");
CASE(DND, "playing");
CASE(XA, "away");
CASE(Unavailable, "offline");
CASE(Probe, "probe");
CASE(Error, "error");
CASE(Invalid, "invalid");
default:
LOGERROR("Unknown presence type '%d'", static_cast(presenceType));
return "";
#undef CASE
}
}
/**
* Convert a gloox role type to an untranslated string literal to be used as an identifier by the scripts.
*/
const char* XmppClient::GetRoleString(const gloox::MUCRoomRole role)
{
switch (role)
{
#define CASE(X, Y) case gloox::X: return Y
CASE(RoleNone, "none");
CASE(RoleVisitor, "visitor");
CASE(RoleParticipant, "participant");
CASE(RoleModerator, "moderator");
CASE(RoleInvalid, "invalid");
default:
LOGERROR("Unknown role type '%d'", static_cast(role));
return "";
#undef CASE
}
}
/**
* Translates a gloox certificate error codes, i.e. gloox certificate statuses except CertOk.
* Keep in sync with specifications.
*/
std::string XmppClient::CertificateErrorToString(gloox::CertStatus status)
{
std::map certificateErrorStrings = {
{ gloox::CertInvalid, g_L10n.Translate("The certificate is not trusted.") },
{ gloox::CertSignerUnknown, g_L10n.Translate("The certificate hasn't got a known issuer.") },
{ gloox::CertRevoked, g_L10n.Translate("The certificate has been revoked.") },
{ gloox::CertExpired, g_L10n.Translate("The certificate has expired.") },
{ gloox::CertNotActive, g_L10n.Translate("The certificate is not yet active.") },
{ gloox::CertWrongPeer, g_L10n.Translate("The certificate has not been issued for the peer connected to.") },
{ gloox::CertSignerNotCa, g_L10n.Translate("The certificate signer is not a certificate authority.") }
};
std::string result;
for (std::map::iterator it = certificateErrorStrings.begin(); it != certificateErrorStrings.end(); ++it)
if (status & it->first)
result += "\n" + it->second;
return result;
}
/**
* Convert a gloox stanza error type to string.
* Keep in sync with Gloox documentation
*
* @param err Error to be converted
* @return Converted error string
*/
std::string XmppClient::StanzaErrorToString(gloox::StanzaError err)
{
#define CASE(X, Y) case gloox::X: return Y
#define DEBUG_CASE(X, Y) case gloox::X: return g_L10n.Translate("Error") + " (" + Y + ")"
switch (err)
{
CASE(StanzaErrorUndefined, g_L10n.Translate("No error"));
DEBUG_CASE(StanzaErrorBadRequest, "Server received malformed XML");
CASE(StanzaErrorConflict, g_L10n.Translate("Player already logged in"));
DEBUG_CASE(StanzaErrorFeatureNotImplemented, "Server does not implement requested feature");
CASE(StanzaErrorForbidden, g_L10n.Translate("Forbidden"));
DEBUG_CASE(StanzaErrorGone, "Unable to find message receipiant");
CASE(StanzaErrorInternalServerError, g_L10n.Translate("Internal server error"));
DEBUG_CASE(StanzaErrorItemNotFound, "Message receipiant does not exist");
DEBUG_CASE(StanzaErrorJidMalformed, "JID (XMPP address) malformed");
DEBUG_CASE(StanzaErrorNotAcceptable, "Receipiant refused message. Possible policy issue");
CASE(StanzaErrorNotAllowed, g_L10n.Translate("Not allowed"));
CASE(StanzaErrorNotAuthorized, g_L10n.Translate("Not authorized"));
DEBUG_CASE(StanzaErrorNotModified, "Requested item has not changed since last request");
DEBUG_CASE(StanzaErrorPaymentRequired, "This server requires payment");
CASE(StanzaErrorRecipientUnavailable, g_L10n.Translate("Recipient temporarily unavailable"));
DEBUG_CASE(StanzaErrorRedirect, "Request redirected");
CASE(StanzaErrorRegistrationRequired, g_L10n.Translate("Registration required"));
DEBUG_CASE(StanzaErrorRemoteServerNotFound, "Remote server not found");
DEBUG_CASE(StanzaErrorRemoteServerTimeout, "Remote server timed out");
DEBUG_CASE(StanzaErrorResourceConstraint, "The recipient is unable to process the message due to resource constraints");
CASE(StanzaErrorServiceUnavailable, g_L10n.Translate("Service unavailable"));
DEBUG_CASE(StanzaErrorSubscribtionRequired, "Service requires subscription");
DEBUG_CASE(StanzaErrorUnexpectedRequest, "Attempt to send from invalid stanza address");
DEBUG_CASE(StanzaErrorUnknownSender, "Invalid 'from' address");
default:
return g_L10n.Translate("Unknown error");
}
#undef DEBUG_CASE
#undef CASE
}
/**
* Convert a gloox connection error enum to string
* Keep in sync with Gloox documentation
*
* @param err Error to be converted
* @return Converted error string
*/
std::string XmppClient::ConnectionErrorToString(gloox::ConnectionError err)
{
#define CASE(X, Y) case gloox::X: return Y
#define DEBUG_CASE(X, Y) case gloox::X: return g_L10n.Translate("Error") + " (" + Y + ")"
switch (err)
{
CASE(ConnNoError, g_L10n.Translate("No error"));
CASE(ConnStreamError, g_L10n.Translate("Stream error"));
CASE(ConnStreamVersionError, g_L10n.Translate("The incoming stream version is unsupported"));
CASE(ConnStreamClosed, g_L10n.Translate("The stream has been closed by the server"));
DEBUG_CASE(ConnProxyAuthRequired, "The HTTP/SOCKS5 proxy requires authentication");
DEBUG_CASE(ConnProxyAuthFailed, "HTTP/SOCKS5 proxy authentication failed");
DEBUG_CASE(ConnProxyNoSupportedAuth, "The HTTP/SOCKS5 proxy requires an unsupported authentication mechanism");
CASE(ConnIoError, g_L10n.Translate("An I/O error occurred"));
DEBUG_CASE(ConnParseError, "An XML parse error occurred");
CASE(ConnConnectionRefused, g_L10n.Translate("The connection was refused by the server"));
CASE(ConnDnsError, g_L10n.Translate("Resolving the server's hostname failed"));
CASE(ConnOutOfMemory, g_L10n.Translate("This system is out of memory"));
DEBUG_CASE(ConnNoSupportedAuth, "The authentication mechanisms the server offered are not supported or no authentication mechanisms were available");
CASE(ConnTlsFailed, g_L10n.Translate("The server's certificate could not be verified or the TLS handshake did not complete successfully"));
CASE(ConnTlsNotAvailable, g_L10n.Translate("The server did not offer required TLS encryption"));
DEBUG_CASE(ConnCompressionFailed, "Negotiation/initializing compression failed");
CASE(ConnAuthenticationFailed, g_L10n.Translate("Authentication failed. Incorrect password or account does not exist"));
CASE(ConnUserDisconnected, g_L10n.Translate("The user or system requested a disconnect"));
CASE(ConnNotConnected, g_L10n.Translate("There is no active connection"));
default:
return g_L10n.Translate("Unknown error");
}
#undef DEBUG_CASE
#undef CASE
}
/**
* Convert a gloox registration result enum to string
* Keep in sync with Gloox documentation
*
* @param err Enum to be converted
* @return Converted string
*/
std::string XmppClient::RegistrationResultToString(gloox::RegistrationResult res)
{
#define CASE(X, Y) case gloox::X: return Y
#define DEBUG_CASE(X, Y) case gloox::X: return g_L10n.Translate("Error") + " (" + Y + ")"
switch (res)
{
CASE(RegistrationSuccess, g_L10n.Translate("Your account has been successfully registered"));
CASE(RegistrationNotAcceptable, g_L10n.Translate("Not all necessary information provided"));
CASE(RegistrationConflict, g_L10n.Translate("Username already exists"));
DEBUG_CASE(RegistrationNotAuthorized, "Account removal timeout or insufficiently secure channel for password change");
DEBUG_CASE(RegistrationBadRequest, "Server received an incomplete request");
DEBUG_CASE(RegistrationForbidden, "Registration forbidden");
DEBUG_CASE(RegistrationRequired, "Account cannot be removed as it does not exist");
DEBUG_CASE(RegistrationUnexpectedRequest, "This client is unregistered with the server");
DEBUG_CASE(RegistrationNotAllowed, "Server does not permit password changes");
default:
return "";
}
#undef DEBUG_CASE
#undef CASE
}
void XmppClient::SendStunEndpointToHost(const StunClient::StunEndpoint& stunEndpoint, const std::string& hostJIDStr)
{
DbgXMPP("SendStunEndpointToHost " << hostJIDStr);
char ipStr[256] = "(error)";
ENetAddress addr;
addr.host = ntohl(stunEndpoint.ip);
enet_address_get_host_ip(&addr, ipStr, ARRAY_SIZE(ipStr));
glooxwrapper::JID hostJID(hostJIDStr);
glooxwrapper::Jingle::Session session = m_sessionManager->createSession(hostJID);
session.sessionInitiate(ipStr, stunEndpoint.port);
}
void XmppClient::handleSessionAction(gloox::Jingle::Action action, glooxwrapper::Jingle::Session& session, const glooxwrapper::Jingle::Session::Jingle& jingle)
{
if (action == gloox::Jingle::SessionInitiate)
handleSessionInitiation(session, jingle);
}
void XmppClient::handleSessionInitiation(glooxwrapper::Jingle::Session& UNUSED(session), const glooxwrapper::Jingle::Session::Jingle& jingle)
{
glooxwrapper::Jingle::ICEUDP::Candidate candidate = jingle.getCandidate();
if (candidate.ip.empty())
{
LOGERROR("Failed to retrieve Jingle candidate");
return;
}
if (!g_NetServer)
{
LOGERROR("Received STUN connection request, but not hosting currently!");
return;
}
g_NetServer->SendHolePunchingMessage(candidate.ip.to_string(), candidate.port);
}
Index: ps/trunk/source/network/NetClient.cpp
===================================================================
--- ps/trunk/source/network/NetClient.cpp (revision 24168)
+++ ps/trunk/source/network/NetClient.cpp (revision 24169)
@@ -1,863 +1,863 @@
/* Copyright (C) 2019 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "NetClient.h"
#include "NetClientTurnManager.h"
#include "NetMessage.h"
#include "NetSession.h"
#include "lib/byte_order.h"
#include "lib/external_libraries/enet.h"
#include "lib/sysdep/sysdep.h"
#include "lobby/IXmppClient.h"
#include "ps/CConsole.h"
#include "ps/CLogger.h"
#include "ps/Compress.h"
#include "ps/CStr.h"
#include "ps/Game.h"
#include "ps/Loader.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/Simulation2.h"
CNetClient *g_NetClient = NULL;
/**
* Async task for receiving the initial game state when rejoining an
* in-progress network game.
*/
class CNetFileReceiveTask_ClientRejoin : public CNetFileReceiveTask
{
NONCOPYABLE(CNetFileReceiveTask_ClientRejoin);
public:
CNetFileReceiveTask_ClientRejoin(CNetClient& client)
: m_Client(client)
{
}
virtual void OnComplete()
{
// We've received the game state from the server
// Save it so we can use it after the map has finished loading
m_Client.m_JoinSyncBuffer = m_Buffer;
// Pretend the server told us to start the game
CGameStartMessage start;
m_Client.HandleMessage(&start);
}
private:
CNetClient& m_Client;
};
CNetClient::CNetClient(CGame* game, bool isLocalClient) :
m_Session(NULL),
m_UserName(L"anonymous"),
m_HostID((u32)-1), m_ClientTurnManager(NULL), m_Game(game),
m_GameAttributes(game->GetSimulation2()->GetScriptInterface().GetContext()),
m_IsLocalClient(isLocalClient),
m_LastConnectionCheck(0),
m_Rejoin(false)
{
m_Game->SetTurnManager(NULL); // delete the old local turn manager so we don't accidentally use it
void* context = this;
JS_AddExtraGCRootsTracer(GetScriptInterface().GetJSRuntime(), CNetClient::Trace, this);
// Set up transitions for session
AddTransition(NCS_UNCONNECTED, (uint)NMT_CONNECT_COMPLETE, NCS_CONNECT, (void*)&OnConnect, context);
AddTransition(NCS_CONNECT, (uint)NMT_SERVER_HANDSHAKE, NCS_HANDSHAKE, (void*)&OnHandshake, context);
AddTransition(NCS_HANDSHAKE, (uint)NMT_SERVER_HANDSHAKE_RESPONSE, NCS_AUTHENTICATE, (void*)&OnHandshakeResponse, context);
AddTransition(NCS_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NCS_AUTHENTICATE, (void*)&OnAuthenticateRequest, context);
AddTransition(NCS_AUTHENTICATE, (uint)NMT_AUTHENTICATE_RESULT, NCS_INITIAL_GAMESETUP, (void*)&OnAuthenticate, context);
AddTransition(NCS_INITIAL_GAMESETUP, (uint)NMT_GAME_SETUP, NCS_PREGAME, (void*)&OnGameSetup, context);
AddTransition(NCS_PREGAME, (uint)NMT_CHAT, NCS_PREGAME, (void*)&OnChat, context);
AddTransition(NCS_PREGAME, (uint)NMT_READY, NCS_PREGAME, (void*)&OnReady, context);
AddTransition(NCS_PREGAME, (uint)NMT_GAME_SETUP, NCS_PREGAME, (void*)&OnGameSetup, context);
AddTransition(NCS_PREGAME, (uint)NMT_PLAYER_ASSIGNMENT, NCS_PREGAME, (void*)&OnPlayerAssignment, context);
AddTransition(NCS_PREGAME, (uint)NMT_KICKED, NCS_PREGAME, (void*)&OnKicked, context);
AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_TIMEOUT, NCS_PREGAME, (void*)&OnClientTimeout, context);
AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_PERFORMANCE, NCS_PREGAME, (void*)&OnClientPerformance, context);
AddTransition(NCS_PREGAME, (uint)NMT_GAME_START, NCS_LOADING, (void*)&OnGameStart, context);
AddTransition(NCS_PREGAME, (uint)NMT_JOIN_SYNC_START, NCS_JOIN_SYNCING, (void*)&OnJoinSyncStart, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CHAT, NCS_JOIN_SYNCING, (void*)&OnChat, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_GAME_SETUP, NCS_JOIN_SYNCING, (void*)&OnGameSetup, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_PLAYER_ASSIGNMENT, NCS_JOIN_SYNCING, (void*)&OnPlayerAssignment, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_KICKED, NCS_JOIN_SYNCING, (void*)&OnKicked, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CLIENT_TIMEOUT, NCS_JOIN_SYNCING, (void*)&OnClientTimeout, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CLIENT_PERFORMANCE, NCS_JOIN_SYNCING, (void*)&OnClientPerformance, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_GAME_START, NCS_JOIN_SYNCING, (void*)&OnGameStart, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_SIMULATION_COMMAND, NCS_JOIN_SYNCING, (void*)&OnInGame, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_END_COMMAND_BATCH, NCS_JOIN_SYNCING, (void*)&OnJoinSyncEndCommandBatch, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NCS_INGAME, (void*)&OnLoadedGame, context);
AddTransition(NCS_LOADING, (uint)NMT_CHAT, NCS_LOADING, (void*)&OnChat, context);
AddTransition(NCS_LOADING, (uint)NMT_GAME_SETUP, NCS_LOADING, (void*)&OnGameSetup, context);
AddTransition(NCS_LOADING, (uint)NMT_PLAYER_ASSIGNMENT, NCS_LOADING, (void*)&OnPlayerAssignment, context);
AddTransition(NCS_LOADING, (uint)NMT_KICKED, NCS_LOADING, (void*)&OnKicked, context);
AddTransition(NCS_LOADING, (uint)NMT_CLIENT_TIMEOUT, NCS_LOADING, (void*)&OnClientTimeout, context);
AddTransition(NCS_LOADING, (uint)NMT_CLIENT_PERFORMANCE, NCS_LOADING, (void*)&OnClientPerformance, context);
AddTransition(NCS_LOADING, (uint)NMT_CLIENTS_LOADING, NCS_LOADING, (void*)&OnClientsLoading, context);
AddTransition(NCS_LOADING, (uint)NMT_LOADED_GAME, NCS_INGAME, (void*)&OnLoadedGame, context);
AddTransition(NCS_INGAME, (uint)NMT_REJOINED, NCS_INGAME, (void*)&OnRejoined, context);
AddTransition(NCS_INGAME, (uint)NMT_KICKED, NCS_INGAME, (void*)&OnKicked, context);
AddTransition(NCS_INGAME, (uint)NMT_CLIENT_TIMEOUT, NCS_INGAME, (void*)&OnClientTimeout, context);
AddTransition(NCS_INGAME, (uint)NMT_CLIENT_PERFORMANCE, NCS_INGAME, (void*)&OnClientPerformance, context);
AddTransition(NCS_INGAME, (uint)NMT_CLIENTS_LOADING, NCS_INGAME, (void*)&OnClientsLoading, context);
AddTransition(NCS_INGAME, (uint)NMT_CLIENT_PAUSED, NCS_INGAME, (void*)&OnClientPaused, context);
AddTransition(NCS_INGAME, (uint)NMT_CHAT, NCS_INGAME, (void*)&OnChat, context);
AddTransition(NCS_INGAME, (uint)NMT_GAME_SETUP, NCS_INGAME, (void*)&OnGameSetup, context);
AddTransition(NCS_INGAME, (uint)NMT_PLAYER_ASSIGNMENT, NCS_INGAME, (void*)&OnPlayerAssignment, context);
AddTransition(NCS_INGAME, (uint)NMT_SIMULATION_COMMAND, NCS_INGAME, (void*)&OnInGame, context);
AddTransition(NCS_INGAME, (uint)NMT_SYNC_ERROR, NCS_INGAME, (void*)&OnInGame, context);
AddTransition(NCS_INGAME, (uint)NMT_END_COMMAND_BATCH, NCS_INGAME, (void*)&OnInGame, context);
// Set first state
SetFirstState(NCS_UNCONNECTED);
}
CNetClient::~CNetClient()
{
DestroyConnection();
JS_RemoveExtraGCRootsTracer(GetScriptInterface().GetJSRuntime(), CNetClient::Trace, this);
}
void CNetClient::TraceMember(JSTracer *trc)
{
for (JS::Heap& guiMessage : m_GuiMessageQueue)
- JS_CallValueTracer(trc, &guiMessage, "m_GuiMessageQueue");
+ JS::TraceEdge(trc, &guiMessage, "m_GuiMessageQueue");
}
void CNetClient::SetUserName(const CStrW& username)
{
ENSURE(!m_Session); // must be called before we start the connection
m_UserName = username;
}
void CNetClient::SetHostingPlayerName(const CStr& hostingPlayerName)
{
m_HostingPlayerName = hostingPlayerName;
}
bool CNetClient::SetupConnection(const CStr& server, const u16 port, ENetHost* enetClient)
{
CNetClientSession* session = new CNetClientSession(*this);
bool ok = session->Connect(server, port, m_IsLocalClient, enetClient);
SetAndOwnSession(session);
return ok;
}
void CNetClient::SetAndOwnSession(CNetClientSession* session)
{
delete m_Session;
m_Session = session;
}
void CNetClient::DestroyConnection()
{
// Attempt to send network messages from the current frame before connection is destroyed.
if (m_ClientTurnManager)
{
m_ClientTurnManager->OnDestroyConnection();
Flush();
}
SAFE_DELETE(m_Session);
}
void CNetClient::Poll()
{
if (!m_Session)
return;
CheckServerConnection();
m_Session->Poll();
}
void CNetClient::CheckServerConnection()
{
// Trigger local warnings if the connection to the server is bad.
// At most once per second.
std::time_t now = std::time(nullptr);
if (now <= m_LastConnectionCheck)
return;
m_LastConnectionCheck = now;
// Report if we are losing the connection to the server
u32 lastReceived = m_Session->GetLastReceivedTime();
if (lastReceived > NETWORK_WARNING_TIMEOUT)
{
PushGuiMessage(
"type", "netwarn",
"warntype", "server-timeout",
"lastReceivedTime", lastReceived);
return;
}
// Report if we have a bad ping to the server
u32 meanRTT = m_Session->GetMeanRTT();
if (meanRTT > DEFAULT_TURN_LENGTH_MP)
{
PushGuiMessage(
"type", "netwarn",
"warntype", "server-latency",
"meanRTT", meanRTT);
}
}
void CNetClient::Flush()
{
if (m_Session)
m_Session->Flush();
}
void CNetClient::GuiPoll(JS::MutableHandleValue ret)
{
if (m_GuiMessageQueue.empty())
{
ret.setUndefined();
return;
}
ret.set(m_GuiMessageQueue.front());
m_GuiMessageQueue.pop_front();
}
std::string CNetClient::TestReadGuiMessages()
{
JSContext* cx = GetScriptInterface().GetContext();
JSAutoRequest rq(cx);
std::string r;
JS::RootedValue msg(cx);
while (true)
{
GuiPoll(&msg);
if (msg.isUndefined())
break;
r += GetScriptInterface().ToString(&msg) + "\n";
}
return r;
}
const ScriptInterface& CNetClient::GetScriptInterface()
{
return m_Game->GetSimulation2()->GetScriptInterface();
}
void CNetClient::PostPlayerAssignmentsToScript()
{
JSContext* cx = GetScriptInterface().GetContext();
JSAutoRequest rq(cx);
JS::RootedValue newAssignments(cx);
ScriptInterface::CreateObject(cx, &newAssignments);
for (const std::pair& p : m_PlayerAssignments)
{
JS::RootedValue assignment(cx);
ScriptInterface::CreateObject(
cx,
&assignment,
"name", p.second.m_Name,
"player", p.second.m_PlayerID,
"status", p.second.m_Status);
GetScriptInterface().SetProperty(newAssignments, p.first.c_str(), assignment);
}
PushGuiMessage(
"type", "players",
"newAssignments", newAssignments);
}
bool CNetClient::SendMessage(const CNetMessage* message)
{
if (!m_Session)
return false;
return m_Session->SendMessage(message);
}
void CNetClient::HandleConnect()
{
Update((uint)NMT_CONNECT_COMPLETE, NULL);
}
void CNetClient::HandleDisconnect(u32 reason)
{
PushGuiMessage(
"type", "netstatus",
"status", "disconnected",
"reason", reason);
SAFE_DELETE(m_Session);
// Update the state immediately to UNCONNECTED (don't bother with FSM transitions since
// we'd need one for every single state, and we don't need to use per-state actions)
SetCurrState(NCS_UNCONNECTED);
}
void CNetClient::SendGameSetupMessage(JS::MutableHandleValue attrs, const ScriptInterface& scriptInterface)
{
CGameSetupMessage gameSetup(scriptInterface);
gameSetup.m_Data = attrs;
SendMessage(&gameSetup);
}
void CNetClient::SendAssignPlayerMessage(const int playerID, const CStr& guid)
{
CAssignPlayerMessage assignPlayer;
assignPlayer.m_PlayerID = playerID;
assignPlayer.m_GUID = guid;
SendMessage(&assignPlayer);
}
void CNetClient::SendChatMessage(const std::wstring& text)
{
CChatMessage chat;
chat.m_Message = text;
SendMessage(&chat);
}
void CNetClient::SendReadyMessage(const int status)
{
CReadyMessage readyStatus;
readyStatus.m_Status = status;
SendMessage(&readyStatus);
}
void CNetClient::SendClearAllReadyMessage()
{
CClearAllReadyMessage clearAllReady;
SendMessage(&clearAllReady);
}
void CNetClient::SendStartGameMessage()
{
CGameStartMessage gameStart;
SendMessage(&gameStart);
}
void CNetClient::SendRejoinedMessage()
{
CRejoinedMessage rejoinedMessage;
SendMessage(&rejoinedMessage);
}
void CNetClient::SendKickPlayerMessage(const CStrW& playerName, bool ban)
{
CKickedMessage kickPlayer;
kickPlayer.m_Name = playerName;
kickPlayer.m_Ban = ban;
SendMessage(&kickPlayer);
}
void CNetClient::SendPausedMessage(bool pause)
{
CClientPausedMessage pausedMessage;
pausedMessage.m_Pause = pause;
SendMessage(&pausedMessage);
}
bool CNetClient::HandleMessage(CNetMessage* message)
{
// Handle non-FSM messages first
Status status = m_Session->GetFileTransferer().HandleMessageReceive(*message);
if (status == INFO::OK)
return true;
if (status != INFO::SKIPPED)
return false;
if (message->GetType() == NMT_FILE_TRANSFER_REQUEST)
{
CFileTransferRequestMessage* reqMessage = static_cast(message);
// TODO: we should support different transfer request types, instead of assuming
// it's always requesting the simulation state
std::stringstream stream;
LOGMESSAGERENDER("Serializing game at turn %u for rejoining player", m_ClientTurnManager->GetCurrentTurn());
u32 turn = to_le32(m_ClientTurnManager->GetCurrentTurn());
stream.write((char*)&turn, sizeof(turn));
bool ok = m_Game->GetSimulation2()->SerializeState(stream);
ENSURE(ok);
// Compress the content with zlib to save bandwidth
// (TODO: if this is still too large, compressing with e.g. LZMA works much better)
std::string compressed;
CompressZLib(stream.str(), compressed, true);
m_Session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, compressed);
return true;
}
// Update FSM
bool ok = Update(message->GetType(), message);
if (!ok)
LOGERROR("Net client: Error running FSM update (type=%d state=%d)", (int)message->GetType(), (int)GetCurrState());
return ok;
}
void CNetClient::LoadFinished()
{
if (!m_JoinSyncBuffer.empty())
{
// We're rejoining a game, and just finished loading the initial map,
// so deserialize the saved game state now
std::string state;
DecompressZLib(m_JoinSyncBuffer, state, true);
std::stringstream stream(state);
u32 turn;
stream.read((char*)&turn, sizeof(turn));
turn = to_le32(turn);
LOGMESSAGE("Rejoining client deserializing state at turn %u\n", turn);
bool ok = m_Game->GetSimulation2()->DeserializeState(stream);
ENSURE(ok);
m_ClientTurnManager->ResetState(turn, turn);
PushGuiMessage(
"type", "netstatus",
"status", "join_syncing");
}
else
{
// Connecting at the start of a game, so we'll wait for other players to finish loading
PushGuiMessage(
"type", "netstatus",
"status", "waiting_for_players");
}
CLoadedGameMessage loaded;
loaded.m_CurrentTurn = m_ClientTurnManager->GetCurrentTurn();
SendMessage(&loaded);
}
void CNetClient::SendAuthenticateMessage()
{
CAuthenticateMessage authenticate;
authenticate.m_Name = m_UserName;
authenticate.m_Password = L""; // TODO
authenticate.m_IsLocalClient = m_IsLocalClient;
SendMessage(&authenticate);
}
bool CNetClient::OnConnect(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CONNECT_COMPLETE);
CNetClient* client = static_cast(context);
client->PushGuiMessage(
"type", "netstatus",
"status", "connected");
return true;
}
bool CNetClient::OnHandshake(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_SERVER_HANDSHAKE);
CNetClient* client = static_cast(context);
CCliHandshakeMessage handshake;
handshake.m_MagicResponse = PS_PROTOCOL_MAGIC_RESPONSE;
handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION;
handshake.m_SoftwareVersion = PS_PROTOCOL_VERSION;
client->SendMessage(&handshake);
return true;
}
bool CNetClient::OnHandshakeResponse(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_SERVER_HANDSHAKE_RESPONSE);
CNetClient* client = static_cast(context);
CSrvHandshakeResponseMessage* message = static_cast(event->GetParamRef());
client->m_GUID = message->m_GUID;
if (message->m_Flags & PS_NETWORK_FLAG_REQUIRE_LOBBYAUTH)
{
if (g_XmppClient && !client->m_HostingPlayerName.empty())
g_XmppClient->SendIqLobbyAuth(client->m_HostingPlayerName, client->m_GUID);
else
{
client->PushGuiMessage(
"type", "netstatus",
"status", "disconnected",
"reason", static_cast(NDR_LOBBY_AUTH_FAILED));
LOGMESSAGE("Net client: Couldn't send lobby auth xmpp message");
}
return true;
}
client->SendAuthenticateMessage();
return true;
}
bool CNetClient::OnAuthenticateRequest(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE);
CNetClient* client = static_cast(context);
client->SendAuthenticateMessage();
return true;
}
bool CNetClient::OnAuthenticate(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE_RESULT);
CNetClient* client = static_cast(context);
CAuthenticateResultMessage* message = static_cast(event->GetParamRef());
LOGMESSAGE("Net: Authentication result: host=%u, %s", message->m_HostID, utf8_from_wstring(message->m_Message));
client->m_HostID = message->m_HostID;
client->m_Rejoin = message->m_Code == ARC_OK_REJOINING;
client->PushGuiMessage(
"type", "netstatus",
"status", "authenticated",
"rejoining", client->m_Rejoin);
return true;
}
bool CNetClient::OnChat(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CHAT);
CNetClient* client = static_cast(context);
CChatMessage* message = static_cast(event->GetParamRef());
client->PushGuiMessage(
"type", "chat",
"guid", message->m_GUID,
"text", message->m_Message);
return true;
}
bool CNetClient::OnReady(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_READY);
CNetClient* client = static_cast(context);
CReadyMessage* message = static_cast(event->GetParamRef());
client->PushGuiMessage(
"type", "ready",
"guid", message->m_GUID,
"status", message->m_Status);
return true;
}
bool CNetClient::OnGameSetup(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_GAME_SETUP);
CNetClient* client = static_cast(context);
CGameSetupMessage* message = static_cast(event->GetParamRef());
client->m_GameAttributes = message->m_Data;
client->PushGuiMessage(
"type", "gamesetup",
"data", message->m_Data);
return true;
}
bool CNetClient::OnPlayerAssignment(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_PLAYER_ASSIGNMENT);
CNetClient* client = static_cast(context);
CPlayerAssignmentMessage* message = static_cast(event->GetParamRef());
// Unpack the message
PlayerAssignmentMap newPlayerAssignments;
for (size_t i = 0; i < message->m_Hosts.size(); ++i)
{
PlayerAssignment assignment;
assignment.m_Enabled = true;
assignment.m_Name = message->m_Hosts[i].m_Name;
assignment.m_PlayerID = message->m_Hosts[i].m_PlayerID;
assignment.m_Status = message->m_Hosts[i].m_Status;
newPlayerAssignments[message->m_Hosts[i].m_GUID] = assignment;
}
client->m_PlayerAssignments.swap(newPlayerAssignments);
client->PostPlayerAssignmentsToScript();
return true;
}
// This is called either when the host clicks the StartGame button or
// if this client rejoins and finishes the download of the simstate.
bool CNetClient::OnGameStart(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_GAME_START);
CNetClient* client = static_cast(context);
client->m_Session->SetLongTimeout(true);
// Find the player assigned to our GUID
int player = -1;
if (client->m_PlayerAssignments.find(client->m_GUID) != client->m_PlayerAssignments.end())
player = client->m_PlayerAssignments[client->m_GUID].m_PlayerID;
client->m_ClientTurnManager = new CNetClientTurnManager(
*client->m_Game->GetSimulation2(), *client, client->m_HostID, client->m_Game->GetReplayLogger());
client->m_Game->SetPlayerID(player);
client->m_Game->StartGame(&client->m_GameAttributes, "");
client->PushGuiMessage("type", "start");
return true;
}
bool CNetClient::OnJoinSyncStart(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_JOIN_SYNC_START);
CNetClient* client = static_cast(context);
// The server wants us to start downloading the game state from it, so do so
client->m_Session->GetFileTransferer().StartTask(
shared_ptr(new CNetFileReceiveTask_ClientRejoin(*client))
);
return true;
}
bool CNetClient::OnJoinSyncEndCommandBatch(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_END_COMMAND_BATCH);
CNetClient* client = static_cast(context);
CEndCommandBatchMessage* endMessage = (CEndCommandBatchMessage*)event->GetParamRef();
client->m_ClientTurnManager->FinishedAllCommands(endMessage->m_Turn, endMessage->m_TurnLength);
// Execute all the received commands for the latest turn
client->m_ClientTurnManager->UpdateFastForward();
return true;
}
bool CNetClient::OnRejoined(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_REJOINED);
CNetClient* client = static_cast(context);
CRejoinedMessage* message = static_cast(event->GetParamRef());
client->PushGuiMessage(
"type", "rejoined",
"guid", message->m_GUID);
return true;
}
bool CNetClient::OnKicked(void *context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_KICKED);
CNetClient* client = static_cast(context);
CKickedMessage* message = static_cast(event->GetParamRef());
client->PushGuiMessage(
"username", message->m_Name,
"type", "kicked",
"banned", message->m_Ban != 0);
return true;
}
bool CNetClient::OnClientTimeout(void *context, CFsmEvent* event)
{
// Report the timeout of some other client
ENSURE(event->GetType() == (uint)NMT_CLIENT_TIMEOUT);
CNetClient* client = static_cast(context);
CClientTimeoutMessage* message = static_cast(event->GetParamRef());
client->PushGuiMessage(
"type", "netwarn",
"warntype", "client-timeout",
"guid", message->m_GUID,
"lastReceivedTime", message->m_LastReceivedTime);
return true;
}
bool CNetClient::OnClientPerformance(void *context, CFsmEvent* event)
{
// Performance statistics for one or multiple clients
ENSURE(event->GetType() == (uint)NMT_CLIENT_PERFORMANCE);
CNetClient* client = static_cast(context);
CClientPerformanceMessage* message = static_cast(event->GetParamRef());
// Display warnings for other clients with bad ping
for (size_t i = 0; i < message->m_Clients.size(); ++i)
{
if (message->m_Clients[i].m_MeanRTT < DEFAULT_TURN_LENGTH_MP || message->m_Clients[i].m_GUID == client->m_GUID)
continue;
client->PushGuiMessage(
"type", "netwarn",
"warntype", "client-latency",
"guid", message->m_Clients[i].m_GUID,
"meanRTT", message->m_Clients[i].m_MeanRTT);
}
return true;
}
bool CNetClient::OnClientsLoading(void *context, CFsmEvent *event)
{
ENSURE(event->GetType() == (uint)NMT_CLIENTS_LOADING);
CNetClient* client = static_cast(context);
CClientsLoadingMessage* message = static_cast(event->GetParamRef());
bool finished = true;
std::vector guids;
guids.reserve(message->m_Clients.size());
for (const CClientsLoadingMessage::S_m_Clients& mClient : message->m_Clients)
{
if (client->m_GUID == mClient.m_GUID)
finished = false;
guids.push_back(mClient.m_GUID);
}
// Disable the timeout here after processing the enet message, so as to ensure that the connection isn't currently
// timing out (as it is when just leaving the loading screen in LoadFinished).
if (finished)
client->m_Session->SetLongTimeout(false);
client->PushGuiMessage(
"type", "clients-loading",
"guids", guids);
return true;
}
bool CNetClient::OnClientPaused(void *context, CFsmEvent *event)
{
ENSURE(event->GetType() == (uint)NMT_CLIENT_PAUSED);
CNetClient* client = static_cast(context);
CClientPausedMessage* message = static_cast(event->GetParamRef());
client->PushGuiMessage(
"type", "paused",
"pause", message->m_Pause != 0,
"guid", message->m_GUID);
return true;
}
bool CNetClient::OnLoadedGame(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
CNetClient* client = static_cast(context);
// All players have loaded the game - start running the turn manager
// so that the game begins
client->m_Game->SetTurnManager(client->m_ClientTurnManager);
client->PushGuiMessage(
"type", "netstatus",
"status", "active");
// If we have rejoined an in progress game, send the rejoined message to the server.
if (client->m_Rejoin)
client->SendRejoinedMessage();
// The last client to leave the loading screen didn't receive the CClientsLoadingMessage, so disable here.
client->m_Session->SetLongTimeout(false);
return true;
}
bool CNetClient::OnInGame(void *context, CFsmEvent* event)
{
// TODO: should split each of these cases into a separate method
CNetClient* client = static_cast(context);
CNetMessage* message = static_cast(event->GetParamRef());
if (message)
{
if (message->GetType() == NMT_SIMULATION_COMMAND)
{
CSimulationMessage* simMessage = static_cast (message);
client->m_ClientTurnManager->OnSimulationMessage(simMessage);
}
else if (message->GetType() == NMT_SYNC_ERROR)
{
CSyncErrorMessage* syncMessage = static_cast (message);
client->m_ClientTurnManager->OnSyncError(syncMessage->m_Turn, syncMessage->m_HashExpected, syncMessage->m_PlayerNames);
}
else if (message->GetType() == NMT_END_COMMAND_BATCH)
{
CEndCommandBatchMessage* endMessage = static_cast (message);
client->m_ClientTurnManager->FinishedAllCommands(endMessage->m_Turn, endMessage->m_TurnLength);
}
}
return true;
}
Index: ps/trunk/source/simulation2/components/CCmpAIManager.cpp
===================================================================
--- ps/trunk/source/simulation2/components/CCmpAIManager.cpp (revision 24168)
+++ ps/trunk/source/simulation2/components/CCmpAIManager.cpp (revision 24169)
@@ -1,1174 +1,1174 @@
/* Copyright (C) 2020 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "simulation2/system/Component.h"
#include "ICmpAIManager.h"
#include "simulation2/MessageTypes.h"
#include "graphics/Terrain.h"
#include "lib/timer.h"
#include "lib/tex/tex.h"
#include "lib/allocators/shared_ptr.h"
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "ps/Profile.h"
#include "ps/scripting/JSInterface_VFS.h"
#include "ps/TemplateLoader.h"
#include "ps/Util.h"
#include "scriptinterface/ScriptRuntime.h"
#include "simulation2/components/ICmpAIInterface.h"
#include "simulation2/components/ICmpCommandQueue.h"
#include "simulation2/components/ICmpObstructionManager.h"
#include "simulation2/components/ICmpRangeManager.h"
#include "simulation2/components/ICmpTemplateManager.h"
#include "simulation2/components/ICmpTerritoryManager.h"
#include "simulation2/helpers/HierarchicalPathfinder.h"
#include "simulation2/helpers/LongPathfinder.h"
#include "simulation2/serialization/DebugSerializer.h"
#include "simulation2/serialization/StdDeserializer.h"
#include "simulation2/serialization/StdSerializer.h"
#include "simulation2/serialization/SerializeTemplates.h"
extern void QuitEngine();
/**
* @file
* Player AI interface.
* AI is primarily scripted, and the CCmpAIManager component defined here
* takes care of managing all the scripts.
*
* To avoid slow AI scripts causing jerky rendering, they are run in a background
* thread (maintained by CAIWorker) so that it's okay if they take a whole simulation
* turn before returning their results (though preferably they shouldn't use nearly
* that much CPU).
*
* CCmpAIManager grabs the world state after each turn (making use of AIInterface.js
* and AIProxy.js to decide what data to include) then passes it to CAIWorker.
* The AI scripts will then run asynchronously and return a list of commands to execute.
* Any attempts to read the command list (including indirectly via serialization)
* will block until it's actually completed, so the rest of the engine should avoid
* reading it for as long as possible.
*
* JS::Values are passed between the game and AI threads using ScriptInterface::StructuredClone.
*
* TODO: actually the thread isn't implemented yet, because performance hasn't been
* sufficiently problematic to justify the complexity yet, but the CAIWorker interface
* is designed to hopefully support threading when we want it.
*/
/**
* Implements worker thread for CCmpAIManager.
*/
class CAIWorker
{
private:
class CAIPlayer
{
NONCOPYABLE(CAIPlayer);
public:
CAIPlayer(CAIWorker& worker, const std::wstring& aiName, player_id_t player, u8 difficulty, const std::wstring& behavior,
shared_ptr scriptInterface) :
m_Worker(worker), m_AIName(aiName), m_Player(player), m_Difficulty(difficulty), m_Behavior(behavior),
m_ScriptInterface(scriptInterface), m_Obj(scriptInterface->GetJSRuntime())
{
}
bool Initialise()
{
// LoadScripts will only load each script once even though we call it for each player
if (!m_Worker.LoadScripts(m_AIName))
return false;
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
OsPath path = L"simulation/ai/" + m_AIName + L"/data.json";
JS::RootedValue metadata(cx);
m_Worker.LoadMetadata(path, &metadata);
if (metadata.isUndefined())
{
LOGERROR("Failed to create AI player: can't find %s", path.string8());
return false;
}
// Get the constructor name from the metadata
std::string moduleName;
std::string constructor;
JS::RootedValue objectWithConstructor(cx); // object that should contain the constructor function
JS::RootedValue global(cx, m_ScriptInterface->GetGlobalObject());
JS::RootedValue ctor(cx);
if (!m_ScriptInterface->HasProperty(metadata, "moduleName"))
{
LOGERROR("Failed to create AI player: %s: missing 'moduleName'", path.string8());
return false;
}
m_ScriptInterface->GetProperty(metadata, "moduleName", moduleName);
if (!m_ScriptInterface->GetProperty(global, moduleName.c_str(), &objectWithConstructor)
|| objectWithConstructor.isUndefined())
{
LOGERROR("Failed to create AI player: %s: can't find the module that should contain the constructor: '%s'", path.string8(), moduleName);
return false;
}
if (!m_ScriptInterface->GetProperty(metadata, "constructor", constructor))
{
LOGERROR("Failed to create AI player: %s: missing 'constructor'", path.string8());
return false;
}
// Get the constructor function from the loaded scripts
if (!m_ScriptInterface->GetProperty(objectWithConstructor, constructor.c_str(), &ctor)
|| ctor.isNull())
{
LOGERROR("Failed to create AI player: %s: can't find constructor '%s'", path.string8(), constructor);
return false;
}
m_ScriptInterface->GetProperty(metadata, "useShared", m_UseSharedComponent);
// Set up the data to pass as the constructor argument
JS::RootedValue settings(cx);
ScriptInterface::CreateObject(
cx,
&settings,
"player", m_Player,
"difficulty", m_Difficulty,
"behavior", m_Behavior);
if (!m_UseSharedComponent)
{
ENSURE(m_Worker.m_HasLoadedEntityTemplates);
m_ScriptInterface->SetProperty(settings, "templates", m_Worker.m_EntityTemplates, false);
}
JS::AutoValueVector argv(cx);
argv.append(settings.get());
m_ScriptInterface->CallConstructor(ctor, argv, &m_Obj);
if (m_Obj.get().isNull())
{
LOGERROR("Failed to create AI player: %s: error calling constructor '%s'", path.string8(), constructor);
return false;
}
return true;
}
void Run(JS::HandleValue state, int playerID)
{
m_Commands.clear();
m_ScriptInterface->CallFunctionVoid(m_Obj, "HandleMessage", state, playerID);
}
// overloaded with a sharedAI part.
// javascript can handle both natively on the same function.
void Run(JS::HandleValue state, int playerID, JS::HandleValue SharedAI)
{
m_Commands.clear();
m_ScriptInterface->CallFunctionVoid(m_Obj, "HandleMessage", state, playerID, SharedAI);
}
void InitAI(JS::HandleValue state, JS::HandleValue SharedAI)
{
m_Commands.clear();
m_ScriptInterface->CallFunctionVoid(m_Obj, "Init", state, m_Player, SharedAI);
}
CAIWorker& m_Worker;
std::wstring m_AIName;
player_id_t m_Player;
u8 m_Difficulty;
std::wstring m_Behavior;
bool m_UseSharedComponent;
// Take care to keep this declaration before heap rooted members. Destructors of heap rooted
// members have to be called before the runtime destructor.
shared_ptr m_ScriptInterface;
JS::PersistentRootedValue m_Obj;
std::vector > m_Commands;
};
public:
struct SCommandSets
{
player_id_t player;
std::vector > commands;
};
CAIWorker() :
m_ScriptInterface(new ScriptInterface("Engine", "AI", g_ScriptRuntime)),
m_TurnNum(0),
m_CommandsComputed(true),
m_HasLoadedEntityTemplates(false),
m_HasSharedComponent(false),
m_EntityTemplates(g_ScriptRuntime->m_rt),
m_SharedAIObj(g_ScriptRuntime->m_rt),
m_PassabilityMapVal(g_ScriptRuntime->m_rt),
m_TerritoryMapVal(g_ScriptRuntime->m_rt)
{
m_ScriptInterface->ReplaceNondeterministicRNG(m_RNG);
m_ScriptInterface->SetCallbackData(static_cast (this));
JS_AddExtraGCRootsTracer(m_ScriptInterface->GetJSRuntime(), Trace, this);
m_ScriptInterface->RegisterFunction("PostCommand");
m_ScriptInterface->RegisterFunction("IncludeModule");
m_ScriptInterface->RegisterFunction("Exit");
m_ScriptInterface->RegisterFunction("ComputePath");
m_ScriptInterface->RegisterFunction, u32, u32, u32, CAIWorker::DumpImage>("DumpImage");
m_ScriptInterface->RegisterFunction("GetTemplate");
JSI_VFS::RegisterScriptFunctions_Simulation(*(m_ScriptInterface.get()));
// Globalscripts may use VFS script functions
m_ScriptInterface->LoadGlobalScripts();
}
~CAIWorker()
{
JS_RemoveExtraGCRootsTracer(m_ScriptInterface->GetJSRuntime(), Trace, this);
}
bool HasLoadedEntityTemplates() const { return m_HasLoadedEntityTemplates; }
bool LoadScripts(const std::wstring& moduleName)
{
// Ignore modules that are already loaded
if (m_LoadedModules.find(moduleName) != m_LoadedModules.end())
return true;
// Mark this as loaded, to prevent it recursively loading itself
m_LoadedModules.insert(moduleName);
// Load and execute *.js
VfsPaths pathnames;
if (vfs::GetPathnames(g_VFS, L"simulation/ai/" + moduleName + L"/", L"*.js", pathnames) < 0)
{
LOGERROR("Failed to load AI scripts for module %s", utf8_from_wstring(moduleName));
return false;
}
for (const VfsPath& path : pathnames)
{
if (!m_ScriptInterface->LoadGlobalScriptFile(path))
{
LOGERROR("Failed to load script %s", path.string8());
return false;
}
}
return true;
}
static void IncludeModule(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name)
{
ENSURE(pCxPrivate->pCBData);
CAIWorker* self = static_cast (pCxPrivate->pCBData);
self->LoadScripts(name);
}
static void PostCommand(ScriptInterface::CxPrivate* pCxPrivate, int playerid, JS::HandleValue cmd)
{
ENSURE(pCxPrivate->pCBData);
CAIWorker* self = static_cast (pCxPrivate->pCBData);
self->PostCommand(playerid, cmd);
}
void PostCommand(int playerid, JS::HandleValue cmd)
{
for (size_t i=0; im_Player == playerid)
{
m_Players[i]->m_Commands.push_back(m_ScriptInterface->WriteStructuredClone(cmd));
return;
}
}
LOGERROR("Invalid playerid in PostCommand!");
}
static JS::Value ComputePath(ScriptInterface::CxPrivate* pCxPrivate,
JS::HandleValue position, JS::HandleValue goal, pass_class_t passClass)
{
ENSURE(pCxPrivate->pCBData);
CAIWorker* self = static_cast (pCxPrivate->pCBData);
JSContext* cx(self->m_ScriptInterface->GetContext());
JSAutoRequest rq(cx);
CFixedVector2D pos, goalPos;
std::vector waypoints;
JS::RootedValue retVal(cx);
self->m_ScriptInterface->FromJSVal(cx, position, pos);
self->m_ScriptInterface->FromJSVal(cx, goal, goalPos);
self->ComputePath(pos, goalPos, passClass, waypoints);
self->m_ScriptInterface->ToJSVal >(cx, &retVal, waypoints);
return retVal;
}
void ComputePath(const CFixedVector2D& pos, const CFixedVector2D& goal, pass_class_t passClass, std::vector& waypoints)
{
WaypointPath ret;
PathGoal pathGoal = { PathGoal::POINT, goal.X, goal.Y };
m_LongPathfinder.ComputePath(m_HierarchicalPathfinder, pos.X, pos.Y, pathGoal, passClass, ret);
for (Waypoint& wp : ret.m_Waypoints)
waypoints.emplace_back(wp.x, wp.z);
}
static CParamNode GetTemplate(ScriptInterface::CxPrivate* pCxPrivate, const std::string& name)
{
ENSURE(pCxPrivate->pCBData);
CAIWorker* self = static_cast (pCxPrivate->pCBData);
return self->GetTemplate(name);
}
CParamNode GetTemplate(const std::string& name)
{
if (!m_TemplateLoader.TemplateExists(name))
return CParamNode(false);
return m_TemplateLoader.GetTemplateFileData(name).GetChild("Entity");
}
static void ExitProgram(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
QuitEngine();
}
/**
* Debug function for AI scripts to dump 2D array data (e.g. terrain tile weights).
*/
static void DumpImage(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& name, const std::vector& data, u32 w, u32 h, u32 max)
{
// TODO: this is totally not threadsafe.
VfsPath filename = L"screenshots/aidump/" + name;
if (data.size() != w*h)
{
debug_warn(L"DumpImage: data size doesn't match w*h");
return;
}
if (max == 0)
{
debug_warn(L"DumpImage: max must not be 0");
return;
}
const size_t bpp = 8;
int flags = TEX_BOTTOM_UP|TEX_GREY;
const size_t img_size = w * h * bpp/8;
const size_t hdr_size = tex_hdr_size(filename);
shared_ptr buf;
AllocateAligned(buf, hdr_size+img_size, maxSectorSize);
Tex t;
if (t.wrap(w, h, bpp, flags, buf, hdr_size) < 0)
return;
u8* img = buf.get() + hdr_size;
for (size_t i = 0; i < data.size(); ++i)
img[i] = (u8)((data[i] * 255) / max);
tex_write(&t, filename);
}
void SetRNGSeed(u32 seed)
{
m_RNG.seed(seed);
}
bool TryLoadSharedComponent()
{
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
// we don't need to load it.
if (!m_HasSharedComponent)
return false;
// reset the value so it can be used to determine if we actually initialized it.
m_HasSharedComponent = false;
if (LoadScripts(L"common-api"))
m_HasSharedComponent = true;
else
return false;
// mainly here for the error messages
OsPath path = L"simulation/ai/common-api/";
// Constructor name is SharedScript, it's in the module API3
// TODO: Hardcoding this is bad, we need a smarter way.
JS::RootedValue AIModule(cx);
JS::RootedValue global(cx, m_ScriptInterface->GetGlobalObject());
JS::RootedValue ctor(cx);
if (!m_ScriptInterface->GetProperty(global, "API3", &AIModule) || AIModule.isUndefined())
{
LOGERROR("Failed to create shared AI component: %s: can't find module '%s'", path.string8(), "API3");
return false;
}
if (!m_ScriptInterface->GetProperty(AIModule, "SharedScript", &ctor)
|| ctor.isUndefined())
{
LOGERROR("Failed to create shared AI component: %s: can't find constructor '%s'", path.string8(), "SharedScript");
return false;
}
// Set up the data to pass as the constructor argument
JS::RootedValue playersID(cx);
ScriptInterface::CreateObject(cx, &playersID);
for (size_t i = 0; i < m_Players.size(); ++i)
{
JS::RootedValue val(cx);
m_ScriptInterface->ToJSVal(cx, &val, m_Players[i]->m_Player);
m_ScriptInterface->SetPropertyInt(playersID, i, val, true);
}
ENSURE(m_HasLoadedEntityTemplates);
JS::RootedValue settings(cx);
ScriptInterface::CreateObject(
cx,
&settings,
"players", playersID,
"templates", m_EntityTemplates);
JS::AutoValueVector argv(cx);
argv.append(settings);
m_ScriptInterface->CallConstructor(ctor, argv, &m_SharedAIObj);
if (m_SharedAIObj.get().isNull())
{
LOGERROR("Failed to create shared AI component: %s: error calling constructor '%s'", path.string8(), "SharedScript");
return false;
}
return true;
}
bool AddPlayer(const std::wstring& aiName, player_id_t player, u8 difficulty, const std::wstring& behavior)
{
shared_ptr ai(new CAIPlayer(*this, aiName, player, difficulty, behavior, m_ScriptInterface));
if (!ai->Initialise())
return false;
// this will be set to true if we need to load the shared Component.
if (!m_HasSharedComponent)
m_HasSharedComponent = ai->m_UseSharedComponent;
m_Players.push_back(ai);
return true;
}
bool RunGamestateInit(const shared_ptr& gameState, const Grid& passabilityMap, const Grid& territoryMap,
const std::map& nonPathfindingPassClassMasks, const std::map& pathfindingPassClassMasks)
{
// this will be run last by InitGame.js, passing the full game representation.
// For now it will run for the shared Component.
// This is NOT run during deserialization.
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue state(cx);
m_ScriptInterface->ReadStructuredClone(gameState, &state);
ScriptInterface::ToJSVal(cx, &m_PassabilityMapVal, passabilityMap);
ScriptInterface::ToJSVal(cx, &m_TerritoryMapVal, territoryMap);
m_PassabilityMap = passabilityMap;
m_NonPathfindingPassClasses = nonPathfindingPassClassMasks;
m_PathfindingPassClasses = pathfindingPassClassMasks;
m_LongPathfinder.Reload(&m_PassabilityMap);
m_HierarchicalPathfinder.Recompute(&m_PassabilityMap, nonPathfindingPassClassMasks, pathfindingPassClassMasks);
if (m_HasSharedComponent)
{
m_ScriptInterface->SetProperty(state, "passabilityMap", m_PassabilityMapVal, true);
m_ScriptInterface->SetProperty(state, "territoryMap", m_TerritoryMapVal, true);
m_ScriptInterface->CallFunctionVoid(m_SharedAIObj, "init", state);
for (size_t i = 0; i < m_Players.size(); ++i)
{
if (m_HasSharedComponent && m_Players[i]->m_UseSharedComponent)
m_Players[i]->InitAI(state, m_SharedAIObj);
}
}
return true;
}
void UpdateGameState(const shared_ptr& gameState)
{
ENSURE(m_CommandsComputed);
m_GameState = gameState;
}
void UpdatePathfinder(const Grid& passabilityMap, bool globallyDirty, const Grid& dirtinessGrid, bool justDeserialized,
const std::map& nonPathfindingPassClassMasks, const std::map& pathfindingPassClassMasks)
{
ENSURE(m_CommandsComputed);
bool dimensionChange = m_PassabilityMap.m_W != passabilityMap.m_W || m_PassabilityMap.m_H != passabilityMap.m_H;
m_PassabilityMap = passabilityMap;
if (globallyDirty)
{
m_LongPathfinder.Reload(&m_PassabilityMap);
m_HierarchicalPathfinder.Recompute(&m_PassabilityMap, nonPathfindingPassClassMasks, pathfindingPassClassMasks);
}
else
{
m_LongPathfinder.Update(&m_PassabilityMap);
m_HierarchicalPathfinder.Update(&m_PassabilityMap, dirtinessGrid);
}
JSContext* cx = m_ScriptInterface->GetContext();
if (dimensionChange || justDeserialized)
ScriptInterface::ToJSVal(cx, &m_PassabilityMapVal, m_PassabilityMap);
else
{
// Avoid a useless memory reallocation followed by a garbage collection.
JSAutoRequest rq(cx);
JS::RootedObject mapObj(cx, &m_PassabilityMapVal.toObject());
JS::RootedValue mapData(cx);
ENSURE(JS_GetProperty(cx, mapObj, "data", &mapData));
JS::RootedObject dataObj(cx, &mapData.toObject());
u32 length = 0;
ENSURE(JS_GetArrayLength(cx, dataObj, &length));
u32 nbytes = (u32)(length * sizeof(NavcellData));
bool sharedMemory;
JS::AutoCheckCannotGC nogc;
memcpy((void*)JS_GetUint16ArrayData(dataObj, &sharedMemory, nogc), m_PassabilityMap.m_Data, nbytes);
}
}
void UpdateTerritoryMap(const Grid& territoryMap)
{
ENSURE(m_CommandsComputed);
bool dimensionChange = m_TerritoryMap.m_W != territoryMap.m_W || m_TerritoryMap.m_H != territoryMap.m_H;
m_TerritoryMap = territoryMap;
JSContext* cx = m_ScriptInterface->GetContext();
if (dimensionChange)
ScriptInterface::ToJSVal(cx, &m_TerritoryMapVal, m_TerritoryMap);
else
{
// Avoid a useless memory reallocation followed by a garbage collection.
JSAutoRequest rq(cx);
JS::RootedObject mapObj(cx, &m_TerritoryMapVal.toObject());
JS::RootedValue mapData(cx);
ENSURE(JS_GetProperty(cx, mapObj, "data", &mapData));
JS::RootedObject dataObj(cx, &mapData.toObject());
u32 length = 0;
ENSURE(JS_GetArrayLength(cx, dataObj, &length));
u32 nbytes = (u32)(length * sizeof(u8));
bool sharedMemory;
JS::AutoCheckCannotGC nogc;
memcpy((void*)JS_GetUint8ArrayData(dataObj, &sharedMemory, nogc), m_TerritoryMap.m_Data, nbytes);
}
}
void StartComputation()
{
m_CommandsComputed = false;
}
void WaitToFinishComputation()
{
if (!m_CommandsComputed)
{
PerformComputation();
m_CommandsComputed = true;
}
}
void GetCommands(std::vector& commands)
{
WaitToFinishComputation();
commands.clear();
commands.resize(m_Players.size());
for (size_t i = 0; i < m_Players.size(); ++i)
{
commands[i].player = m_Players[i]->m_Player;
commands[i].commands = m_Players[i]->m_Commands;
}
}
void LoadEntityTemplates(const std::vector >& templates)
{
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
m_HasLoadedEntityTemplates = true;
ScriptInterface::CreateObject(cx, &m_EntityTemplates);
JS::RootedValue val(cx);
for (size_t i = 0; i < templates.size(); ++i)
{
templates[i].second->ToJSVal(cx, false, &val);
m_ScriptInterface->SetProperty(m_EntityTemplates, templates[i].first.c_str(), val, true);
}
}
void Serialize(std::ostream& stream, bool isDebug)
{
WaitToFinishComputation();
if (isDebug)
{
CDebugSerializer serializer(*m_ScriptInterface, stream);
serializer.Indent(4);
SerializeState(serializer);
}
else
{
CStdSerializer serializer(*m_ScriptInterface, stream);
SerializeState(serializer);
}
}
void SerializeState(ISerializer& serializer)
{
if (m_Players.empty())
return;
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
std::stringstream rngStream;
rngStream << m_RNG;
serializer.StringASCII("rng", rngStream.str(), 0, 32);
serializer.NumberU32_Unbounded("turn", m_TurnNum);
serializer.Bool("useSharedScript", m_HasSharedComponent);
if (m_HasSharedComponent)
{
JS::RootedValue sharedData(cx);
if (!m_ScriptInterface->CallFunction(m_SharedAIObj, "Serialize", &sharedData))
LOGERROR("AI shared script Serialize call failed");
serializer.ScriptVal("sharedData", &sharedData);
}
for (size_t i = 0; i < m_Players.size(); ++i)
{
serializer.String("name", m_Players[i]->m_AIName, 1, 256);
serializer.NumberI32_Unbounded("player", m_Players[i]->m_Player);
serializer.NumberU8_Unbounded("difficulty", m_Players[i]->m_Difficulty);
serializer.String("behavior", m_Players[i]->m_Behavior, 1, 256);
serializer.NumberU32_Unbounded("num commands", (u32)m_Players[i]->m_Commands.size());
for (size_t j = 0; j < m_Players[i]->m_Commands.size(); ++j)
{
JS::RootedValue val(cx);
m_ScriptInterface->ReadStructuredClone(m_Players[i]->m_Commands[j], &val);
serializer.ScriptVal("command", &val);
}
bool hasCustomSerialize = m_ScriptInterface->HasProperty(m_Players[i]->m_Obj, "Serialize");
if (hasCustomSerialize)
{
JS::RootedValue scriptData(cx);
if (!m_ScriptInterface->CallFunction(m_Players[i]->m_Obj, "Serialize", &scriptData))
LOGERROR("AI script Serialize call failed");
serializer.ScriptVal("data", &scriptData);
}
else
{
serializer.ScriptVal("data", &m_Players[i]->m_Obj);
}
}
// AI pathfinder
SerializeMap()(serializer, "non pathfinding pass classes", m_NonPathfindingPassClasses);
SerializeMap()(serializer, "pathfinding pass classes", m_PathfindingPassClasses);
serializer.NumberU16_Unbounded("pathfinder grid w", m_PassabilityMap.m_W);
serializer.NumberU16_Unbounded("pathfinder grid h", m_PassabilityMap.m_H);
serializer.RawBytes("pathfinder grid data", (const u8*)m_PassabilityMap.m_Data,
m_PassabilityMap.m_W*m_PassabilityMap.m_H*sizeof(NavcellData));
}
void Deserialize(std::istream& stream, u32 numAis)
{
m_PlayerMetadata.clear();
m_Players.clear();
if (numAis == 0)
return;
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
ENSURE(m_CommandsComputed); // deserializing while we're still actively computing would be bad
CStdDeserializer deserializer(*m_ScriptInterface, stream);
std::string rngString;
std::stringstream rngStream;
deserializer.StringASCII("rng", rngString, 0, 32);
rngStream << rngString;
rngStream >> m_RNG;
deserializer.NumberU32_Unbounded("turn", m_TurnNum);
deserializer.Bool("useSharedScript", m_HasSharedComponent);
if (m_HasSharedComponent)
{
TryLoadSharedComponent();
JS::RootedValue sharedData(cx);
deserializer.ScriptVal("sharedData", &sharedData);
if (!m_ScriptInterface->CallFunctionVoid(m_SharedAIObj, "Deserialize", sharedData))
LOGERROR("AI shared script Deserialize call failed");
}
for (size_t i = 0; i < numAis; ++i)
{
std::wstring name;
player_id_t player;
u8 difficulty;
std::wstring behavior;
deserializer.String("name", name, 1, 256);
deserializer.NumberI32_Unbounded("player", player);
deserializer.NumberU8_Unbounded("difficulty",difficulty);
deserializer.String("behavior", behavior, 1, 256);
if (!AddPlayer(name, player, difficulty, behavior))
throw PSERROR_Deserialize_ScriptError();
u32 numCommands;
deserializer.NumberU32_Unbounded("num commands", numCommands);
m_Players.back()->m_Commands.reserve(numCommands);
for (size_t j = 0; j < numCommands; ++j)
{
JS::RootedValue val(cx);
deserializer.ScriptVal("command", &val);
m_Players.back()->m_Commands.push_back(m_ScriptInterface->WriteStructuredClone(val));
}
bool hasCustomDeserialize = m_ScriptInterface->HasProperty(m_Players.back()->m_Obj, "Deserialize");
if (hasCustomDeserialize)
{
JS::RootedValue scriptData(cx);
deserializer.ScriptVal("data", &scriptData);
if (m_Players[i]->m_UseSharedComponent)
{
if (!m_ScriptInterface->CallFunctionVoid(m_Players.back()->m_Obj, "Deserialize", scriptData, m_SharedAIObj))
LOGERROR("AI script Deserialize call failed");
}
else if (!m_ScriptInterface->CallFunctionVoid(m_Players.back()->m_Obj, "Deserialize", scriptData))
{
LOGERROR("AI script deserialize() call failed");
}
}
else
{
deserializer.ScriptVal("data", &m_Players.back()->m_Obj);
}
}
// AI pathfinder
SerializeMap()(deserializer, "non pathfinding pass classes", m_NonPathfindingPassClasses);
SerializeMap()(deserializer, "pathfinding pass classes", m_PathfindingPassClasses);
u16 mapW, mapH;
deserializer.NumberU16_Unbounded("pathfinder grid w", mapW);
deserializer.NumberU16_Unbounded("pathfinder grid h", mapH);
m_PassabilityMap = Grid(mapW, mapH);
deserializer.RawBytes("pathfinder grid data", (u8*)m_PassabilityMap.m_Data, mapW*mapH*sizeof(NavcellData));
m_LongPathfinder.Reload(&m_PassabilityMap);
m_HierarchicalPathfinder.Recompute(&m_PassabilityMap, m_NonPathfindingPassClasses, m_PathfindingPassClasses);
}
int getPlayerSize()
{
return m_Players.size();
}
private:
static void Trace(JSTracer *trc, void *data)
{
reinterpret_cast(data)->TraceMember(trc);
}
void TraceMember(JSTracer *trc)
{
for (std::pair>& metadata : m_PlayerMetadata)
- JS_CallValueTracer(trc, &metadata.second, "CAIWorker::m_PlayerMetadata");
+ JS::TraceEdge(trc, &metadata.second, "CAIWorker::m_PlayerMetadata");
}
void LoadMetadata(const VfsPath& path, JS::MutableHandleValue out)
{
if (m_PlayerMetadata.find(path) == m_PlayerMetadata.end())
{
// Load and cache the AI player metadata
m_ScriptInterface->ReadJSONFile(path, out);
m_PlayerMetadata[path] = JS::Heap(out);
return;
}
out.set(m_PlayerMetadata[path].get());
}
void PerformComputation()
{
// Deserialize the game state, to pass to the AI's HandleMessage
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue state(cx);
{
PROFILE3("AI compute read state");
m_ScriptInterface->ReadStructuredClone(m_GameState, &state);
m_ScriptInterface->SetProperty(state, "passabilityMap", m_PassabilityMapVal, true);
m_ScriptInterface->SetProperty(state, "territoryMap", m_TerritoryMapVal, true);
}
// It would be nice to do
// m_ScriptInterface->FreezeObject(state.get(), true);
// to prevent AI scripts accidentally modifying the state and
// affecting other AI scripts they share it with. But the performance
// cost is far too high, so we won't do that.
// If there is a shared component, run it
if (m_HasSharedComponent)
{
PROFILE3("AI run shared component");
m_ScriptInterface->CallFunctionVoid(m_SharedAIObj, "onUpdate", state);
}
for (size_t i = 0; i < m_Players.size(); ++i)
{
PROFILE3("AI script");
PROFILE2_ATTR("player: %d", m_Players[i]->m_Player);
PROFILE2_ATTR("script: %ls", m_Players[i]->m_AIName.c_str());
if (m_HasSharedComponent && m_Players[i]->m_UseSharedComponent)
m_Players[i]->Run(state, m_Players[i]->m_Player, m_SharedAIObj);
else
m_Players[i]->Run(state, m_Players[i]->m_Player);
}
}
// Take care to keep this declaration before heap rooted members. Destructors of heap rooted
// members have to be called before the runtime destructor.
shared_ptr m_ScriptRuntime;
shared_ptr m_ScriptInterface;
boost::rand48 m_RNG;
u32 m_TurnNum;
JS::PersistentRootedValue m_EntityTemplates;
bool m_HasLoadedEntityTemplates;
std::map > m_PlayerMetadata;
std::vector > m_Players; // use shared_ptr just to avoid copying
bool m_HasSharedComponent;
JS::PersistentRootedValue m_SharedAIObj;
std::vector m_Commands;
std::set m_LoadedModules;
shared_ptr m_GameState;
Grid m_PassabilityMap;
JS::PersistentRootedValue m_PassabilityMapVal;
Grid m_TerritoryMap;
JS::PersistentRootedValue m_TerritoryMapVal;
std::map m_NonPathfindingPassClasses;
std::map m_PathfindingPassClasses;
HierarchicalPathfinder m_HierarchicalPathfinder;
LongPathfinder m_LongPathfinder;
bool m_CommandsComputed;
CTemplateLoader m_TemplateLoader;
};
/**
* Implementation of ICmpAIManager.
*/
class CCmpAIManager : public ICmpAIManager
{
public:
static void ClassInit(CComponentManager& UNUSED(componentManager))
{
}
DEFAULT_COMPONENT_ALLOCATOR(AIManager)
static std::string GetSchema()
{
return "";
}
virtual void Init(const CParamNode& UNUSED(paramNode))
{
m_TerritoriesDirtyID = 0;
m_TerritoriesDirtyBlinkingID = 0;
m_JustDeserialized = false;
}
virtual void Deinit()
{
}
virtual void Serialize(ISerializer& serialize)
{
serialize.NumberU32_Unbounded("num ais", m_Worker.getPlayerSize());
// Because the AI worker uses its own ScriptInterface, we can't use the
// ISerializer (which was initialised with the simulation ScriptInterface)
// directly. So we'll just grab the ISerializer's stream and write to it
// with an independent serializer.
m_Worker.Serialize(serialize.GetStream(), serialize.IsDebug());
}
virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize)
{
Init(paramNode);
u32 numAis;
deserialize.NumberU32_Unbounded("num ais", numAis);
if (numAis > 0)
LoadUsedEntityTemplates();
m_Worker.Deserialize(deserialize.GetStream(), numAis);
m_JustDeserialized = true;
}
virtual void AddPlayer(const std::wstring& id, player_id_t player, u8 difficulty, const std::wstring& behavior)
{
LoadUsedEntityTemplates();
m_Worker.AddPlayer(id, player, difficulty, behavior);
// AI players can cheat and see through FoW/SoD, since that greatly simplifies
// their implementation.
// (TODO: maybe cleverer AIs should be able to optionally retain FoW/SoD)
CmpPtr cmpRangeManager(GetSystemEntity());
if (cmpRangeManager)
cmpRangeManager->SetLosRevealAll(player, true);
}
virtual void SetRNGSeed(u32 seed)
{
m_Worker.SetRNGSeed(seed);
}
virtual void TryLoadSharedComponent()
{
m_Worker.TryLoadSharedComponent();
}
virtual void RunGamestateInit()
{
const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
CmpPtr cmpAIInterface(GetSystemEntity());
ENSURE(cmpAIInterface);
// Get the game state from AIInterface
// We flush events from the initialization so we get a clean state now.
JS::RootedValue state(cx);
cmpAIInterface->GetFullRepresentation(&state, true);
// Get the passability data
Grid dummyGrid;
const Grid* passabilityMap = &dummyGrid;
CmpPtr cmpPathfinder(GetSystemEntity());
if (cmpPathfinder)
passabilityMap = &cmpPathfinder->GetPassabilityGrid();
// Get the territory data
// Since getting the territory grid can trigger a recalculation, we check NeedUpdateAI first
Grid dummyGrid2;
const Grid* territoryMap = &dummyGrid2;
CmpPtr cmpTerritoryManager(GetSystemEntity());
if (cmpTerritoryManager && cmpTerritoryManager->NeedUpdateAI(&m_TerritoriesDirtyID, &m_TerritoriesDirtyBlinkingID))
territoryMap = &cmpTerritoryManager->GetTerritoryGrid();
LoadPathfinderClasses(state);
std::map nonPathfindingPassClassMasks, pathfindingPassClassMasks;
if (cmpPathfinder)
cmpPathfinder->GetPassabilityClasses(nonPathfindingPassClassMasks, pathfindingPassClassMasks);
m_Worker.RunGamestateInit(scriptInterface.WriteStructuredClone(state), *passabilityMap, *territoryMap, nonPathfindingPassClassMasks, pathfindingPassClassMasks);
}
virtual void StartComputation()
{
PROFILE("AI setup");
const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
if (m_Worker.getPlayerSize() == 0)
return;
CmpPtr cmpAIInterface(GetSystemEntity());
ENSURE(cmpAIInterface);
// Get the game state from AIInterface
JS::RootedValue state(cx);
if (m_JustDeserialized)
cmpAIInterface->GetFullRepresentation(&state, false);
else
cmpAIInterface->GetRepresentation(&state);
LoadPathfinderClasses(state); // add the pathfinding classes to it
// Update the game state
m_Worker.UpdateGameState(scriptInterface.WriteStructuredClone(state));
// Update the pathfinding data
CmpPtr cmpPathfinder(GetSystemEntity());
if (cmpPathfinder)
{
const GridUpdateInformation& dirtinessInformations = cmpPathfinder->GetAIPathfinderDirtinessInformation();
if (dirtinessInformations.dirty || m_JustDeserialized)
{
const Grid& passabilityMap = cmpPathfinder->GetPassabilityGrid();
std::map nonPathfindingPassClassMasks, pathfindingPassClassMasks;
cmpPathfinder->GetPassabilityClasses(nonPathfindingPassClassMasks, pathfindingPassClassMasks);
m_Worker.UpdatePathfinder(passabilityMap,
dirtinessInformations.globallyDirty, dirtinessInformations.dirtinessGrid, m_JustDeserialized,
nonPathfindingPassClassMasks, pathfindingPassClassMasks);
}
cmpPathfinder->FlushAIPathfinderDirtinessInformation();
}
// Update the territory data
// Since getting the territory grid can trigger a recalculation, we check NeedUpdateAI first
CmpPtr cmpTerritoryManager(GetSystemEntity());
if (cmpTerritoryManager && (cmpTerritoryManager->NeedUpdateAI(&m_TerritoriesDirtyID, &m_TerritoriesDirtyBlinkingID) || m_JustDeserialized))
{
const Grid& territoryMap = cmpTerritoryManager->GetTerritoryGrid();
m_Worker.UpdateTerritoryMap(territoryMap);
}
m_Worker.StartComputation();
m_JustDeserialized = false;
}
virtual void PushCommands()
{
std::vector commands;
m_Worker.GetCommands(commands);
CmpPtr cmpCommandQueue(GetSystemEntity());
if (!cmpCommandQueue)
return;
const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedValue clonedCommandVal(cx);
for (size_t i = 0; i < commands.size(); ++i)
{
for (size_t j = 0; j < commands[i].commands.size(); ++j)
{
scriptInterface.ReadStructuredClone(commands[i].commands[j], &clonedCommandVal);
cmpCommandQueue->PushLocalCommand(commands[i].player, clonedCommandVal);
}
}
}
private:
size_t m_TerritoriesDirtyID;
size_t m_TerritoriesDirtyBlinkingID;
bool m_JustDeserialized;
/**
* Load the templates of all entities on the map (called when adding a new AI player for a new game
* or when deserializing)
*/
void LoadUsedEntityTemplates()
{
if (m_Worker.HasLoadedEntityTemplates())
return;
CmpPtr cmpTemplateManager(GetSystemEntity());
ENSURE(cmpTemplateManager);
std::vector templateNames = cmpTemplateManager->FindUsedTemplates();
std::vector > usedTemplates;
usedTemplates.reserve(templateNames.size());
for (const std::string& name : templateNames)
{
const CParamNode* node = cmpTemplateManager->GetTemplateWithoutValidation(name);
if (node)
usedTemplates.emplace_back(name, node);
}
// Send the data to the worker
m_Worker.LoadEntityTemplates(usedTemplates);
}
void LoadPathfinderClasses(JS::HandleValue state)
{
CmpPtr cmpPathfinder(GetSystemEntity());
if (!cmpPathfinder)
return;
const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedValue classesVal(cx);
ScriptInterface::CreateObject(cx, &classesVal);
std::map classes;
cmpPathfinder->GetPassabilityClasses(classes);
for (std::map::iterator it = classes.begin(); it != classes.end(); ++it)
scriptInterface.SetProperty(classesVal, it->first.c_str(), it->second, true);
scriptInterface.SetProperty(state, "passabilityClasses", classesVal, true);
}
CAIWorker m_Worker;
};
REGISTER_COMPONENT_TYPE(AIManager)
Index: ps/trunk/source/simulation2/serialization/StdDeserializer.cpp
===================================================================
--- ps/trunk/source/simulation2/serialization/StdDeserializer.cpp (revision 24168)
+++ ps/trunk/source/simulation2/serialization/StdDeserializer.cpp (revision 24169)
@@ -1,449 +1,449 @@
/* Copyright (C) 2020 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "StdDeserializer.h"
#include "SerializedScriptTypes.h"
#include "StdSerializer.h" // for DEBUG_SERIALIZER_ANNOTATE
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/ScriptExtraHeaders.h" // for typed arrays
#include "lib/byte_order.h"
CStdDeserializer::CStdDeserializer(const ScriptInterface& scriptInterface, std::istream& stream) :
m_ScriptInterface(scriptInterface), m_Stream(stream)
{
JS_AddExtraGCRootsTracer(m_ScriptInterface.GetJSRuntime(), CStdDeserializer::Trace, this);
}
CStdDeserializer::~CStdDeserializer()
{
JS_RemoveExtraGCRootsTracer(m_ScriptInterface.GetJSRuntime(), CStdDeserializer::Trace, this);
}
void CStdDeserializer::Trace(JSTracer *trc, void *data)
{
reinterpret_cast(data)->TraceMember(trc);
}
void CStdDeserializer::TraceMember(JSTracer *trc)
{
- for (size_t i=0; i& backref : m_ScriptBackrefs)
+ JS::TraceEdge(trc, &backref, "StdDeserializer::m_ScriptBackrefs");
}
void CStdDeserializer::Get(const char* name, u8* data, size_t len)
{
#if DEBUG_SERIALIZER_ANNOTATE
std::string strName;
char c = m_Stream.get();
ENSURE(c == '<');
while (1)
{
c = m_Stream.get();
if (c == '>')
break;
else
strName += c;
}
ENSURE(strName == name);
#else
UNUSED2(name);
#endif
m_Stream.read((char*)data, (std::streamsize)len);
if (!m_Stream.good())
{
// hit eof before len, or other errors
// NOTE: older libc++ versions incorrectly set eofbit on the last char; test gcount as a workaround
// see https://llvm.org/bugs/show_bug.cgi?id=9335
if (m_Stream.bad() || m_Stream.fail() || (m_Stream.eof() && m_Stream.gcount() != (std::streamsize)len))
throw PSERROR_Deserialize_ReadFailed();
}
}
std::istream& CStdDeserializer::GetStream()
{
return m_Stream;
}
void CStdDeserializer::RequireBytesInStream(size_t numBytes)
{
// It would be nice to do:
// if (numBytes > (size_t)m_Stream.rdbuf()->in_avail())
// throw PSERROR_Deserialize_OutOfBounds("RequireBytesInStream");
// but that doesn't work (at least on MSVC) since in_avail isn't
// guaranteed to return the actual number of bytes available; see e.g.
// http://social.msdn.microsoft.com/Forums/en/vclanguage/thread/13009a88-933f-4be7-bf3d-150e425e66a6#70ea562d-8605-4742-8851-1bae431ce6ce
// Instead we'll just verify that it's not an extremely large number:
if (numBytes > 64*MiB)
throw PSERROR_Deserialize_OutOfBounds("RequireBytesInStream");
}
void CStdDeserializer::AddScriptBackref(JS::HandleObject obj)
{
m_ScriptBackrefs.push_back(JS::Heap(obj));
}
void CStdDeserializer::GetScriptBackref(size_t tag, JS::MutableHandleObject ret)
{
ENSURE(m_ScriptBackrefs.size() > tag);
ret.set(m_ScriptBackrefs[tag]);
}
////////////////////////////////////////////////////////////////
JS::Value CStdDeserializer::ReadScriptVal(const char* UNUSED(name), JS::HandleObject appendParent)
{
JSContext* cx = m_ScriptInterface.GetContext();
JSAutoRequest rq(cx);
uint8_t type;
NumberU8_Unbounded("type", type);
switch (type)
{
case SCRIPT_TYPE_VOID:
return JS::UndefinedValue();
case SCRIPT_TYPE_NULL:
return JS::NullValue();
case SCRIPT_TYPE_ARRAY:
case SCRIPT_TYPE_OBJECT:
{
JS::RootedObject obj(cx);
if (appendParent)
{
obj.set(appendParent);
}
else if (type == SCRIPT_TYPE_ARRAY)
{
u32 length;
NumberU32_Unbounded("array length", length);
obj.set(JS_NewArrayObject(cx, length));
}
else // SCRIPT_TYPE_OBJECT
{
obj.set(JS_NewPlainObject(cx));
}
if (!obj)
throw PSERROR_Deserialize_ScriptError("Deserializer failed to create new object");
AddScriptBackref(obj);
uint32_t numProps;
NumberU32_Unbounded("num props", numProps);
bool isLatin1;
for (uint32_t i = 0; i < numProps; ++i)
{
Bool("isLatin1", isLatin1);
if (isLatin1)
{
std::vector propname;
ReadStringLatin1("prop name", propname);
JS::RootedValue propval(cx, ReadScriptVal("prop value", nullptr));
utf16string prp(propname.begin(), propname.end());;
// TODO: Should ask upstream about getting a variant of JS_SetProperty with a length param.
if (!JS_SetUCProperty(cx, obj, (const char16_t*)prp.data(), prp.length(), propval))
throw PSERROR_Deserialize_ScriptError();
}
else
{
utf16string propname;
ReadStringUTF16("prop name", propname);
JS::RootedValue propval(cx, ReadScriptVal("prop value", nullptr));
if (!JS_SetUCProperty(cx, obj, (const char16_t*)propname.data(), propname.length(), propval))
throw PSERROR_Deserialize_ScriptError();
}
}
return JS::ObjectValue(*obj);
}
case SCRIPT_TYPE_STRING:
{
JS::RootedString str(cx);
ScriptString("string", &str);
return JS::StringValue(str);
}
case SCRIPT_TYPE_INT:
{
int32_t value;
NumberI32("value", value, JSVAL_INT_MIN, JSVAL_INT_MAX);
return JS::NumberValue(value);
}
case SCRIPT_TYPE_DOUBLE:
{
double value;
NumberDouble_Unbounded("value", value);
JS::RootedValue rval(cx, JS::NumberValue(value));
if (rval.isNull())
throw PSERROR_Deserialize_ScriptError("JS_NewNumberValue failed");
return rval;
}
case SCRIPT_TYPE_BOOLEAN:
{
uint8_t value;
NumberU8("value", value, 0, 1);
return JS::BooleanValue(value ? true : false);
}
case SCRIPT_TYPE_BACKREF:
{
i32 tag;
NumberI32("tag", tag, 0, JSVAL_INT_MAX);
JS::RootedObject obj(cx);
GetScriptBackref(tag, &obj);
if (!obj)
throw PSERROR_Deserialize_ScriptError("Invalid backref tag");
return JS::ObjectValue(*obj);
}
case SCRIPT_TYPE_OBJECT_NUMBER:
{
double value;
NumberDouble_Unbounded("value", value);
JS::RootedValue val(cx, JS::NumberValue(value));
JS::RootedObject ctorobj(cx);
if (!JS_GetClassObject(cx, JSProto_Number, &ctorobj))
throw PSERROR_Deserialize_ScriptError("JS_GetClassObject failed");
JS::RootedObject obj(cx, JS_New(cx, ctorobj, JS::HandleValueArray(val)));
if (!obj)
throw PSERROR_Deserialize_ScriptError("JS_New failed");
AddScriptBackref(obj);
return JS::ObjectValue(*obj);
}
case SCRIPT_TYPE_OBJECT_STRING:
{
JS::RootedString str(cx);
ScriptString("value", &str);
if (!str)
throw PSERROR_Deserialize_ScriptError();
JS::RootedValue val(cx, JS::StringValue(str));
JS::RootedObject ctorobj(cx);
if (!JS_GetClassObject(cx, JSProto_String, &ctorobj))
throw PSERROR_Deserialize_ScriptError("JS_GetClassObject failed");
JS::RootedObject obj(cx, JS_New(cx, ctorobj, JS::HandleValueArray(val)));
if (!obj)
throw PSERROR_Deserialize_ScriptError("JS_New failed");
AddScriptBackref(obj);
return JS::ObjectValue(*obj);
}
case SCRIPT_TYPE_OBJECT_BOOLEAN:
{
bool value;
Bool("value", value);
JS::RootedValue val(cx, JS::BooleanValue(value));
JS::RootedObject ctorobj(cx);
if (!JS_GetClassObject(cx, JSProto_Boolean, &ctorobj))
throw PSERROR_Deserialize_ScriptError("JS_GetClassObject failed");
JS::RootedObject obj(cx, JS_New(cx, ctorobj, JS::HandleValueArray(val)));
if (!obj)
throw PSERROR_Deserialize_ScriptError("JS_New failed");
AddScriptBackref(obj);
return JS::ObjectValue(*obj);
}
case SCRIPT_TYPE_TYPED_ARRAY:
{
u8 arrayType;
u32 byteOffset, length;
NumberU8_Unbounded("array type", arrayType);
NumberU32_Unbounded("byte offset", byteOffset);
NumberU32_Unbounded("length", length);
// To match the serializer order, we reserve the typed array's backref tag here
JS::RootedObject arrayObj(cx);
AddScriptBackref(arrayObj);
// Get buffer object
JS::RootedValue bufferVal(cx, ReadScriptVal("buffer", nullptr));
if (!bufferVal.isObject())
throw PSERROR_Deserialize_ScriptError();
JS::RootedObject bufferObj(cx, &bufferVal.toObject());
if (!JS_IsArrayBufferObject(bufferObj))
throw PSERROR_Deserialize_ScriptError("js_IsArrayBuffer failed");
switch(arrayType)
{
case SCRIPT_TYPED_ARRAY_INT8:
arrayObj = JS_NewInt8ArrayWithBuffer(cx, bufferObj, byteOffset, length);
break;
case SCRIPT_TYPED_ARRAY_UINT8:
arrayObj = JS_NewUint8ArrayWithBuffer(cx, bufferObj, byteOffset, length);
break;
case SCRIPT_TYPED_ARRAY_INT16:
arrayObj = JS_NewInt16ArrayWithBuffer(cx, bufferObj, byteOffset, length);
break;
case SCRIPT_TYPED_ARRAY_UINT16:
arrayObj = JS_NewUint16ArrayWithBuffer(cx, bufferObj, byteOffset, length);
break;
case SCRIPT_TYPED_ARRAY_INT32:
arrayObj = JS_NewInt32ArrayWithBuffer(cx, bufferObj, byteOffset, length);
break;
case SCRIPT_TYPED_ARRAY_UINT32:
arrayObj = JS_NewUint32ArrayWithBuffer(cx, bufferObj, byteOffset, length);
break;
case SCRIPT_TYPED_ARRAY_FLOAT32:
arrayObj = JS_NewFloat32ArrayWithBuffer(cx, bufferObj, byteOffset, length);
break;
case SCRIPT_TYPED_ARRAY_FLOAT64:
arrayObj = JS_NewFloat64ArrayWithBuffer(cx, bufferObj, byteOffset, length);
break;
case SCRIPT_TYPED_ARRAY_UINT8_CLAMPED:
arrayObj = JS_NewUint8ClampedArrayWithBuffer(cx, bufferObj, byteOffset, length);
break;
default:
throw PSERROR_Deserialize_ScriptError("Failed to deserialize unrecognized typed array view");
}
if (!arrayObj)
throw PSERROR_Deserialize_ScriptError("js_CreateTypedArrayWithBuffer failed");
return JS::ObjectValue(*arrayObj);
}
case SCRIPT_TYPE_ARRAY_BUFFER:
{
u32 length;
NumberU32_Unbounded("buffer length", length);
#if BYTE_ORDER != LITTLE_ENDIAN
#error TODO: need to convert JS ArrayBuffer data from little-endian
#endif
void* contents = malloc(length);
ENSURE(contents);
RawBytes("buffer data", (u8*)contents, length);
JS::RootedObject bufferObj(cx, JS_NewArrayBufferWithContents(cx, length, contents));
AddScriptBackref(bufferObj);
return JS::ObjectValue(*bufferObj);
}
case SCRIPT_TYPE_OBJECT_MAP:
{
JS::RootedObject obj(cx, JS::NewMapObject(cx));
AddScriptBackref(obj);
u32 mapSize;
NumberU32_Unbounded("map size", mapSize);
for (u32 i=0; i& str)
{
uint32_t len;
NumberU32_Unbounded("string length", len);
RequireBytesInStream(len);
str.resize(len);
Get(name, (u8*)str.data(), len);
}
void CStdDeserializer::ReadStringUTF16(const char* name, utf16string& str)
{
uint32_t len;
NumberU32_Unbounded("string length", len);
RequireBytesInStream(len*2);
str.resize(len);
Get(name, (u8*)str.data(), len*2);
}
void CStdDeserializer::ScriptString(const char* name, JS::MutableHandleString out)
{
#if BYTE_ORDER != LITTLE_ENDIAN
#error TODO: probably need to convert JS strings from little-endian
#endif
JSContext* cx = m_ScriptInterface.GetContext();
JSAutoRequest rq(cx);
bool isLatin1;
Bool("isLatin1", isLatin1);
if (isLatin1)
{
std::vector str;
ReadStringLatin1(name, str);
out.set(JS_NewStringCopyN(cx, (const char*)str.data(), str.size()));
if (!out)
throw PSERROR_Deserialize_ScriptError("JS_NewStringCopyN failed");
}
else
{
utf16string str;
ReadStringUTF16(name, str);
out.set(JS_NewUCStringCopyN(cx, (const char16_t*)str.data(), str.length()));
if (!out)
throw PSERROR_Deserialize_ScriptError("JS_NewUCStringCopyN failed");
}
}
void CStdDeserializer::ScriptVal(const char* name, JS::MutableHandleValue out)
{
out.set(ReadScriptVal(name, nullptr));
}
void CStdDeserializer::ScriptObjectAppend(const char* name, JS::HandleValue objVal)
{
JSContext* cx = m_ScriptInterface.GetContext();
JSAutoRequest rq(cx);
if (!objVal.isObject())
throw PSERROR_Deserialize_ScriptError();
JS::RootedObject obj(cx, &objVal.toObject());
ReadScriptVal(name, obj);
}