#pragma once /// @defgroup Tokens Token List /// @brief Data structure for tracking decentralized state across multiple tasks. /// @{ /// /// Token objects can be created using @ref TokenList::MakeToken(), returning a shared pointer to a new Token. This /// new Token can then be added to the TokenList using @ref TokenList::AddToken(). @ref TokenList::TakeToken() /// can be used to make + add a new token with a single function call. /// /// Because TokenList uses weak pointers to track its elements, Token objects are logically removed from the list once /// they are destroyed. As such, it is usually unnecessary to explicitly call @ref TokenList::RemoveToken() to remove a /// Token from the list. Instead, it is idiomatic to consider the Token to be a sort of "scope guard" that will remove /// itself from all TokenList objects when it leaves scope. /// /// The TokenList class is included as part of Squid::Tasks to provide a simple mechanism for robustly sharing aribtrary /// state between multiple tasks. Consider this example of a poison damage-over-time system: /// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} /// /// class Character : public Actor /// { /// public: /// bool IsPoisoned() const /// { /// return m_poisonTokens; // Whether there are any live poison tokens /// } /// /// void OnPoisoned(float in_dps, float in_duration) /// { /// m_taskMgr.RunManaged(ManagePoisonInstance(in_dps, in_duration)); /// } /// /// private: /// TokenList m_poisonTokens; // Token list indicating live poison damage /// /// Task<> ManagePoisonInstance(float in_dps, float in_duration) /// { /// // Take a poison token and hold it for N seconds /// auto poisonToken = m_poisonTokens.TakeToken(__FUNCTION__, in_dps); /// co_await WaitSeconds(in_duration); /// } /// /// Task<> ManageCharacter() // Called once per frame /// { /// while(true) /// { /// float poisonDps = m_poisonTokens.GetMax(); // Get highest DPS poison instance /// DealDamage(poisonDps * GetDT()); // Deal the actual poison damage /// co_await Suspend(); /// } /// } /// }; /// /// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /// /// As the above example shows, this mechanism is well-suited for coroutines, as they can hold a Token across /// multiple frames. Also note that Token objects can optionally hold data. The TokenList class has query functions /// (e.g. GetMin()/GetMax()) that can be used to aggregate the data from the set of live tokens. This is used above /// to quickly find the highest DPS poison instance. #include #include #include #include //--- User configuration header ---// #include "TasksConfig.h" NAMESPACE_SQUID_BEGIN template class TokenList; /// @brief Handle to a TokenList element that stores a debug name /// @details In most circumstances, name should be set to \ref __FUNCTION__ at the point of creation. struct Token { Token(std::string in_name) : name(std::move(in_name)) { } std::string name; // Used for debug only }; /// @brief Handle to a TokenList element that stores both a debug name and associated data /// @details In most circumstances, name should be set to \c __FUNCTION__ at the point of creation. template struct DataToken { DataToken(std::string in_name, tData in_data) : name(std::move(in_name)) , data(std::move(in_data)) { } std::string name; // Used for debug only tData data; }; /// Create a token with the specified debug name inline std::shared_ptr MakeToken(std::string in_name) { return std::make_shared(std::move(in_name)); } /// Create a token with the specified debug name and associated data template std::shared_ptr> MakeToken(std::string in_name, tData in_data) { return std::make_shared>(std::move(in_name), std::move(in_data)); } /// @brief Container for tracking decentralized state across multiple tasks. (See \ref Tokens for more info...) /// @tparam T Type of data to associate with each Token in this container template class TokenList { public: /// Type of Token tracked by this container using Token = typename std::conditional_t::value, Token, DataToken>; /// Create a token with the specified debug name template ::value>* = nullptr> static std::shared_ptr MakeToken(std::string in_name) { return std::make_shared(std::move(in_name)); } /// Create a token with the specified debug name and associated data template ::value>* = nullptr> static std::shared_ptr MakeToken(std::string in_name, U in_data) { return std::make_shared(std::move(in_name), std::move(in_data)); } /// Create and add a token with the specified debug name template ::value>* = nullptr> SQUID_NODISCARD std::shared_ptr TakeToken(std::string in_name) { return AddTokenInternal(MakeToken(std::move(in_name))); } /// Create and add a token with the specified debug name and associated data template ::value>* = nullptr> SQUID_NODISCARD std::shared_ptr TakeToken(std::string in_name, U in_data) { return AddTokenInternal(MakeToken(std::move(in_name), std::move(in_data))); } /// Add an existing token to this container std::shared_ptr AddToken(std::shared_ptr in_token) { SQUID_RUNTIME_CHECK(in_token, "Cannot add null token"); auto foundIter = std::find_if(m_tokens.begin(), m_tokens.end(), [&in_token](const std::weak_ptr in_iterToken){ return in_iterToken.lock() == in_token; }); if(foundIter == m_tokens.end()) // Prevent duplicate tokens { return AddTokenInternal(in_token); } return in_token; } /// Explicitly remove a token from this container void RemoveToken(std::shared_ptr in_token) { // Find and remove the token if(m_tokens.size()) { m_tokens.erase(std::remove_if(m_tokens.begin(), m_tokens.end(), [&in_token](const std::weak_ptr& in_otherToken) { return !in_otherToken.owner_before(in_token) && !in_token.owner_before(in_otherToken); }), m_tokens.end()); } } /// Convenience conversion operator that calls HasTokens() operator bool() const { return HasTokens(); } /// Returns whether this container holds any live tokens bool HasTokens() const { // Return true when holding any unexpired tokens for(auto i = (int32_t)(m_tokens.size() - 1); i >= 0; --i) { const auto& token = m_tokens[i]; if(!token.expired()) { return true; } m_tokens.pop_back(); // Because the token is expired, we can safely remove it from the back } return false; } /// Returns an array of all live token data std::vector GetTokenData() const { std::vector tokenData; for(const auto& tokenWeak : m_tokens) { if(auto token = tokenWeak.lock()) { tokenData.push_back(token->data); } } return tokenData; } /// @name Data Queries /// Methods for querying and aggregating the data from the set of live tokens. /// @{ /// Returns associated data from the least-recently-added live token std::optional GetLeastRecent() const { Sanitize(); return m_tokens.size() ? m_tokens.front().lock()->data : std::optional{}; } /// Returns associated data from the most-recently-added live token std::optional GetMostRecent() const { Sanitize(); return m_tokens.size() ? m_tokens.back().lock()->data : std::optional{}; } /// Returns smallest associated data from the set of live tokens std::optional GetMin() const { std::optional ret; SanitizeAndProcessData([&ret](const T& in_data) { if(!ret || in_data < ret.value()) { ret = in_data; } }); return ret; } /// Returns largest associated data from the set of live tokens std::optional GetMax() const { std::optional ret; SanitizeAndProcessData([&ret](const T& in_data) { if(!ret || in_data > ret.value()) { ret = in_data; } }); return ret; } /// Returns arithmetic mean of all associated data from the set of live tokens std::optional GetMean() const { std::optional ret; std::optional total; SanitizeAndProcessData([&total](const T& in_data) { total = total.value_or(0.0) + (double)in_data; }); if(total) { ret = total.value() / m_tokens.size(); } return ret; } /// Returns whether the set of live tokens contains at least one token associated with the specified data template ::value>* = nullptr> bool Contains(const U& in_searchData) const { bool containsData = false; SanitizeAndProcessData([&in_searchData, &containsData](const T& in_data) { if(in_searchData == in_data) { containsData = true; } }); return containsData; } ///@} end of Data Queries /// Returns a debug string containing a list of the debug names of all live tokens std::string GetDebugString() const { std::vector tokenStrings; std::string debugStr; for(const auto& token : m_tokens) { if(!token.expired()) { if(debugStr.size() > 0) { debugStr += "\n"; } debugStr += token.lock()->name; } } if(debugStr.size() == 0) { debugStr = "[no tokens]"; } return debugStr; } private: // Shared internal implementation for adding tokens std::shared_ptr AddTokenInternal(std::shared_ptr in_token) { Sanitize(); m_tokens.push_back(in_token); return in_token; } // Sanitation void Sanitize() const { // Remove all invalid tokens if(m_tokens.size()) { m_tokens.erase(std::remove_if(m_tokens.begin(), m_tokens.end(), [](const std::weak_ptr& in_token) { return in_token.expired(); }), m_tokens.end()); } } template void SanitizeAndProcessData(tFn in_dataFn) const { // Remove all invalid tokens while applying a processing function on each valid token if(m_tokens.size()) { m_tokens.erase(std::remove_if(m_tokens.begin(), m_tokens.end(), [&in_dataFn](const std::weak_ptr& in_token) { if(in_token.expired()) { return true; } if(auto token = in_token.lock()) { in_dataFn(token->data); } return false; }), m_tokens.end()); } } // Token data mutable std::vector> m_tokens; // Mutable so we can remove expired tokens while converting bool }; NAMESPACE_SQUID_END ///@} end of Tokens group