From c9e76efe7b2bcb991556402031d0176b439d60f0 Mon Sep 17 00:00:00 2001 From: elexis Date: Mon, 30 Dec 2024 12:14:34 +0100 Subject: [PATCH] Store whether a player is activ in C++ This prevents mods from mutating this value and revealing the map. Part of this commit is written by @phosit. --- .../public/simulation/components/Player.js | 5 +- .../components/interfaces/Player.js | 12 ----- source/ps/Game.cpp | 5 +- source/simulation2/MessageTypes.h | 34 ++++++++++++- source/simulation2/TypeList.h | 4 +- source/simulation2/components/ICmpPlayer.cpp | 51 ++++++++++++++++++- source/simulation2/components/ICmpPlayer.h | 5 +- .../scripting/MessageTypeConversions.cpp | 34 ++++++++++++- .../simulation2/scripting/ScriptComponent.cpp | 9 +++- .../simulation2/scripting/ScriptComponent.h | 33 +++++++----- .../simulation2/system/ComponentManager.cpp | 24 ++++++++- source/simulation2/system/ComponentManager.h | 10 ++++ 12 files changed, 187 insertions(+), 39 deletions(-) diff --git a/binaries/data/mods/public/simulation/components/Player.js b/binaries/data/mods/public/simulation/components/Player.js index 242e34e373..f90f779c72 100644 --- a/binaries/data/mods/public/simulation/components/Player.js +++ b/binaries/data/mods/public/simulation/components/Player.js @@ -512,6 +512,9 @@ Player.prototype.SetState = function(newState, message) }); } + Engine.PostMessage(this.entity, won ? MT_PlayerWon : MT_PlayerDefeated, + { "playerId": this.playerID }); + if (message) Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({ "type": won ? "won" : "defeat", @@ -519,8 +522,6 @@ Player.prototype.SetState = function(newState, message) "allies": [this.playerID], "message": message }); - - Engine.PostMessage(this.entity, won ? MT_PlayerWon : MT_PlayerDefeated, { "playerId": this.playerID }); }; Player.prototype.GetFormations = function() diff --git a/binaries/data/mods/public/simulation/components/interfaces/Player.js b/binaries/data/mods/public/simulation/components/interfaces/Player.js index 2ca8ae9086..9441ba7034 100644 --- a/binaries/data/mods/public/simulation/components/interfaces/Player.js +++ b/binaries/data/mods/public/simulation/components/interfaces/Player.js @@ -10,18 +10,6 @@ Engine.RegisterMessageType("DisabledTechnologiesChanged"); */ Engine.RegisterMessageType("DisabledTemplatesChanged"); -/** - * Message of the form { "playerID": number } - * sent from Player component when a player is defeated. - */ -Engine.RegisterMessageType("PlayerDefeated"); - -/** - * Message of the form { "playerID": number } - * sent from Player component when a player has won. - */ -Engine.RegisterMessageType("PlayerWon"); - /** * Message of the form { "to": number, "from": number, "amounts": object } * sent from Player component whenever a tribute is sent. diff --git a/source/ps/Game.cpp b/source/ps/Game.cpp index 304dca1e65..acd736a978 100644 --- a/source/ps/Game.cpp +++ b/source/ps/Game.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2024 Wildfire Games. +/* Copyright (C) 2025 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -486,6 +486,7 @@ bool CGame::IsGameFinished() const return false; } +// This function is implemented so that mods can't change it's result. See ICmpPlayer.cpp bool CGame::PlayerFinished(player_id_t playerID) const { CmpPtr cmpPlayerManager(*m_Simulation2, SYSTEM_ENTITY); @@ -493,5 +494,5 @@ bool CGame::PlayerFinished(player_id_t playerID) const return false; CmpPtr cmpPlayer(*m_Simulation2, cmpPlayerManager->GetPlayerByID(playerID)); - return cmpPlayer && cmpPlayer->GetState() != "active"; + return cmpPlayer && !cmpPlayer->IsActive(); } diff --git a/source/simulation2/MessageTypes.h b/source/simulation2/MessageTypes.h index d872491bfe..daff821997 100644 --- a/source/simulation2/MessageTypes.h +++ b/source/simulation2/MessageTypes.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2023 Wildfire Games. +/* Copyright (C) 2025 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -511,6 +511,38 @@ public: player_id_t player; }; +/** + * Sent by the Player component when a specific player has won. + */ +class CMessagePlayerWon final : public CMessage +{ +public: + DEFAULT_MESSAGE_IMPL(PlayerWon) + + CMessagePlayerWon(player_id_t playerId) : + playerId{playerId} + { + } + + player_id_t playerId; +}; + +/** + * Sent by the Player component when an specific player has been defeated. + */ +class CMessagePlayerDefeated final : public CMessage +{ +public: + DEFAULT_MESSAGE_IMPL(PlayerDefeated) + + CMessagePlayerDefeated(player_id_t playerId) : + playerId{playerId} + { + } + + player_id_t playerId; +}; + /** * Sent by aura and tech managers when a value of a certain template's component is changed */ diff --git a/source/simulation2/TypeList.h b/source/simulation2/TypeList.h index 163205609f..266b1ff7d0 100644 --- a/source/simulation2/TypeList.h +++ b/source/simulation2/TypeList.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2023 Wildfire Games. +/* Copyright (C) 2025 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -61,6 +61,8 @@ MESSAGE(MinimapPing) MESSAGE(CinemaPathEnded) MESSAGE(CinemaQueueEnded) MESSAGE(PlayerColorChanged) +MESSAGE(PlayerWon) +MESSAGE(PlayerDefeated) // TemplateManager must come before all other (non-test) components, // so that it is the first to be (de)serialized diff --git a/source/simulation2/components/ICmpPlayer.cpp b/source/simulation2/components/ICmpPlayer.cpp index a039ef8d5a..20b56dbc80 100644 --- a/source/simulation2/components/ICmpPlayer.cpp +++ b/source/simulation2/components/ICmpPlayer.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2025 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -21,6 +21,7 @@ #include "graphics/Color.h" #include "maths/FixedVector3D.h" +#include "simulation2/system/Component.h" #include "simulation2/system/InterfaceScripted.h" #include "simulation2/scripting/ScriptComponent.h" @@ -30,7 +31,41 @@ END_INTERFACE_WRAPPER(Player) class CCmpPlayerScripted : public ICmpPlayer { public: - DEFAULT_SCRIPT_WRAPPER(PlayerScripted) + DEFAULT_SCRIPT_WRAPPER_BASIC(PlayerScripted) + + static void ClassInit(CComponentManager& componentManager) + { + componentManager.SubscribeToMessageType(MT_PlayerWon); + componentManager.SubscribeToMessageType(MT_PlayerDefeated); + } + + void Serialize(ISerializer& serialize) final + { + serialize.Bool("isActive", m_IsActive); + m_Script.Serialize(serialize); + } + + void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) final + { + deserialize.Bool("isActive", m_IsActive); + m_Script.Deserialize(paramNode, deserialize, GetEntityId()); + } + + void HandleMessage(const CMessage& msg, bool global) final + { + const int msgType{msg.GetType()}; + + // Handle messages that were subscribed to in ClassInit. + if (msgType == MT_PlayerWon || msgType == MT_PlayerDefeated) + { + m_IsActive = false; + if (!m_Script.HasMessageHandler(msg, global)) + return; + } + + // Handle messages that were subscribed to within the JS implementation of the interface. + m_Script.HandleMessage(msg, global); + } CColor GetDisplayedColor() override { @@ -56,6 +91,18 @@ public: { return m_Script.Call("GetState"); } + + bool IsActive() final + { + return m_IsActive; + } + +private: + // Serialize this player state variable in C++ so that mods can't manipulate this value in order to + // reveal the map locally. + // Once it's set to `false` it's never set to true again. To prevent mods from temporarily changing + // it. + bool m_IsActive{true}; }; REGISTER_COMPONENT_SCRIPT_WRAPPER(PlayerScripted) diff --git a/source/simulation2/components/ICmpPlayer.h b/source/simulation2/components/ICmpPlayer.h index cfa9384d0c..412de2f28a 100644 --- a/source/simulation2/components/ICmpPlayer.h +++ b/source/simulation2/components/ICmpPlayer.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2025 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -39,6 +39,9 @@ public: virtual bool HasStartingCamera() = 0; virtual std::string GetState() = 0; + // See the cpp file for why this is implemented in C++. + virtual bool IsActive() = 0; + DECLARE_INTERFACE_TYPE(Player) }; diff --git a/source/simulation2/scripting/MessageTypeConversions.cpp b/source/simulation2/scripting/MessageTypeConversions.cpp index 14cf3a8396..0d18976898 100644 --- a/source/simulation2/scripting/MessageTypeConversions.cpp +++ b/source/simulation2/scripting/MessageTypeConversions.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2023 Wildfire Games. +/* Copyright (C) 2024 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -541,6 +541,38 @@ CMessage* CMessagePlayerColorChanged::FromJSVal(const ScriptRequest& rq, JS::Han return new CMessagePlayerColorChanged(player); } +//////////////////////////////// + +JS::Value CMessagePlayerWon::ToJSVal(const ScriptRequest& rq) const +{ + TOJSVAL_SETUP(); + SET_MSG_PROPERTY(playerId); + return JS::ObjectValue(*obj); +} + +CMessage* CMessagePlayerWon::FromJSVal(const ScriptRequest& rq, JS::HandleValue val) +{ + FROMJSVAL_SETUP(); + GET_MSG_PROPERTY(player_id_t, playerId); + return new CMessagePlayerWon(playerId); +} + +//////////////////////////////// + +JS::Value CMessagePlayerDefeated::ToJSVal(const ScriptRequest& rq) const +{ + TOJSVAL_SETUP(); + SET_MSG_PROPERTY(playerId); + return JS::ObjectValue(*obj); +} + +CMessage* CMessagePlayerDefeated::FromJSVal(const ScriptRequest& rq, JS::HandleValue val) +{ + FROMJSVAL_SETUP(); + GET_MSG_PROPERTY(player_id_t, playerId); + return new CMessagePlayerDefeated(playerId); +} + //////////////////////////////////////////////////////////////// CMessage* CMessageFromJSVal(int mtid, const ScriptRequest& rq, JS::HandleValue val) diff --git a/source/simulation2/scripting/ScriptComponent.cpp b/source/simulation2/scripting/ScriptComponent.cpp index 42bd6264ee..be4d79143f 100644 --- a/source/simulation2/scripting/ScriptComponent.cpp +++ b/source/simulation2/scripting/ScriptComponent.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2023 Wildfire Games. +/* Copyright (C) 2024 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -46,6 +46,13 @@ void CComponentTypeScript::Deinit() ScriptFunction::CallVoid(rq, m_Instance, "Deinit"); } +bool CComponentTypeScript::HasMessageHandler(const CMessage& msg, const bool global) +{ + const ScriptRequest rq(m_ScriptInterface); + return Script::HasProperty(rq, m_Instance, global ? msg.GetScriptGlobalHandlerName() : + msg.GetScriptHandlerName()); +} + void CComponentTypeScript::HandleMessage(const CMessage& msg, bool global) { ScriptRequest rq(m_ScriptInterface); diff --git a/source/simulation2/scripting/ScriptComponent.h b/source/simulation2/scripting/ScriptComponent.h index 845d052a2e..b9cc227eb3 100644 --- a/source/simulation2/scripting/ScriptComponent.h +++ b/source/simulation2/scripting/ScriptComponent.h @@ -41,6 +41,7 @@ public: void Init(const CParamNode& paramNode, entity_id_t ent); void Deinit(); + bool HasMessageHandler(const CMessage& msg, const bool global); void HandleMessage(const CMessage& msg, bool global); void Serialize(ISerializer& serialize); @@ -88,8 +89,7 @@ private: } -#define DEFAULT_SCRIPT_WRAPPER(cname) \ - static void ClassInit(CComponentManager& UNUSED(componentManager)) { } \ +#define DEFAULT_SCRIPT_WRAPPER_BASIC(cname) \ static IComponent* Allocate(const ScriptInterface& scriptInterface, JS::HandleValue instance) \ { \ return new CCmp##cname(scriptInterface, instance); \ @@ -111,18 +111,6 @@ private: { \ m_Script.Deinit(); \ } \ - void HandleMessage(const CMessage& msg, bool global) override \ - { \ - m_Script.HandleMessage(msg, global); \ - } \ - void Serialize(ISerializer& serialize) override \ - { \ - m_Script.Serialize(serialize); \ - } \ - void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) override \ - { \ - m_Script.Deserialize(paramNode, deserialize, GetEntityId()); \ - } \ JS::Value GetJSInstance() const override \ { \ return m_Script.GetInstance(); \ @@ -135,4 +123,21 @@ private: CComponentTypeScript m_Script; \ public: + +#define DEFAULT_SCRIPT_WRAPPER(cname) \ + static void ClassInit(CComponentManager& UNUSED(componentManager)) { } \ + void HandleMessage(const CMessage& msg, bool global) override \ + { \ + m_Script.HandleMessage(msg, global); \ + } \ + void Serialize(ISerializer& serialize) override \ + { \ + m_Script.Serialize(serialize); \ + } \ + void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) override \ + { \ + m_Script.Deserialize(paramNode, deserialize, GetEntityId()); \ + } \ + DEFAULT_SCRIPT_WRAPPER_BASIC(cname) + #endif // INCLUDED_SCRIPTCOMPONENT diff --git a/source/simulation2/system/ComponentManager.cpp b/source/simulation2/system/ComponentManager.cpp index 152e6b135a..432a58568c 100644 --- a/source/simulation2/system/ComponentManager.cpp +++ b/source/simulation2/system/ComponentManager.cpp @@ -20,6 +20,7 @@ #include "ComponentManager.h" #include "lib/utf8.h" +#include "ps/algorithm.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Profile.h" @@ -303,10 +304,17 @@ void CComponentManager::Script_RegisterComponentType_Common(int iid, const std:: return; } + // If we have already subscribed in classInit, do not subscribe again if (isGlobal) - SubscribeGloballyToMessageType(mit->second); + { + if (!IsGloballySubscribed(mit->second)) + SubscribeGloballyToMessageType(mit->second); + } else - SubscribeToMessageType(mit->second); + { + if (!IsLocallySubscribed(mit->second)) + SubscribeToMessageType(mit->second); + } } m_CurrentComponent = CID__Invalid; @@ -571,6 +579,18 @@ void CComponentManager::SubscribeGloballyToMessageType(MessageTypeId mtid) std::sort(types.begin(), types.end()); // TODO: just sort once at the end of LoadComponents } +bool CComponentManager::IsLocallySubscribed(MessageTypeId mtid) +{ + ENSURE(m_CurrentComponent != CID__Invalid); + return PS::contains(m_LocalMessageSubscriptions[mtid], m_CurrentComponent); +} + +bool CComponentManager::IsGloballySubscribed(MessageTypeId mtid) +{ + ENSURE(m_CurrentComponent != CID__Invalid); + return PS::contains(m_GlobalMessageSubscriptions[mtid], m_CurrentComponent); +} + void CComponentManager::FlattenDynamicSubscriptions() { std::map::iterator it; diff --git a/source/simulation2/system/ComponentManager.h b/source/simulation2/system/ComponentManager.h index 8efa35a57e..f7484b9e27 100644 --- a/source/simulation2/system/ComponentManager.h +++ b/source/simulation2/system/ComponentManager.h @@ -113,6 +113,16 @@ public: */ void SubscribeGloballyToMessageType(MessageTypeId mtid); + /** + * Check if the current component type is subscribed to messages of the given type. + */ + bool IsLocallySubscribed(MessageTypeId mtid); + + /** + * Check if the current component type is globally subscribed to messages of the given type. + */ + bool IsGloballySubscribed(MessageTypeId mtid); + /** * Subscribe the given component instance to all messages of the given message type. * The component's HandleMessage will be called on any BroadcastMessage or PostMessage of