356 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			356 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| #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<float> 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 <algorithm>
 | |
| #include <numeric>
 | |
| #include <vector>
 | |
| #include <string>
 | |
| 
 | |
| //--- User configuration header ---//
 | |
| #include "TasksConfig.h"
 | |
| 
 | |
| NAMESPACE_SQUID_BEGIN
 | |
| 
 | |
| template <typename T = void>
 | |
| 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 <typename tData>
 | |
| 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<Token> MakeToken(std::string in_name)
 | |
| {
 | |
| 	return std::make_shared<Token>(std::move(in_name));
 | |
| }
 | |
| 
 | |
| /// Create a token with the specified debug name and associated data
 | |
| template <typename tData>
 | |
| std::shared_ptr<DataToken<tData>> MakeToken(std::string in_name, tData in_data)
 | |
| {
 | |
| 	return std::make_shared<DataToken<tData>>(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 <typename T>
 | |
| class TokenList
 | |
| {
 | |
| public:
 | |
| 	/// Type of Token tracked by this container
 | |
| 	using Token = typename std::conditional_t<std::is_void<T>::value, Token, DataToken<T>>;
 | |
| 
 | |
| 	/// Create a token with the specified debug name
 | |
| 	template <typename U = T, typename std::enable_if_t<std::is_void<U>::value>* = nullptr>
 | |
| 	static std::shared_ptr<Token> MakeToken(std::string in_name)
 | |
| 	{
 | |
| 		return std::make_shared<Token>(std::move(in_name));
 | |
| 	}
 | |
| 
 | |
| 	/// Create a token with the specified debug name and associated data
 | |
| 	template <typename U = T, typename std::enable_if_t<!std::is_void<U>::value>* = nullptr>
 | |
| 	static std::shared_ptr<Token> MakeToken(std::string in_name, U in_data)
 | |
| 	{
 | |
| 		return std::make_shared<Token>(std::move(in_name), std::move(in_data));
 | |
| 	}
 | |
| 
 | |
| 	/// Create and add a token with the specified debug name
 | |
| 	template <typename U = T, typename std::enable_if_t<std::is_void<U>::value>* = nullptr>
 | |
| 	SQUID_NODISCARD std::shared_ptr<Token> 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 <typename U = T, typename std::enable_if_t<!std::is_void<U>::value>* = nullptr>
 | |
| 	SQUID_NODISCARD std::shared_ptr<Token> 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<Token> AddToken(std::shared_ptr<Token> 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<Token> 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<Token> 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<Token>& 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<T> GetTokenData() const
 | |
| 	{
 | |
| 		std::vector<T> 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<T> GetLeastRecent() const
 | |
| 	{
 | |
| 		Sanitize();
 | |
| 		return m_tokens.size() ? m_tokens.front().lock()->data : std::optional<T>{};
 | |
| 	}
 | |
| 
 | |
| 	/// Returns associated data from the most-recently-added live token
 | |
| 	std::optional<T> GetMostRecent() const
 | |
| 	{
 | |
| 		Sanitize();
 | |
| 		return m_tokens.size() ? m_tokens.back().lock()->data : std::optional<T>{};
 | |
| 	}
 | |
| 
 | |
| 	/// Returns smallest associated data from the set of live tokens
 | |
| 	std::optional<T> GetMin() const
 | |
| 	{
 | |
| 		std::optional<T> 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<T> GetMax() const
 | |
| 	{
 | |
| 		std::optional<T> 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<double> GetMean() const
 | |
| 	{
 | |
| 		std::optional<double> ret;
 | |
| 		std::optional<double> 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 <typename U = T, typename std::enable_if_t<!std::is_void<U>::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<std::string> 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<Token> AddTokenInternal(std::shared_ptr<Token> 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<Token>& in_token) {
 | |
| 				return in_token.expired();
 | |
| 			}), m_tokens.end());
 | |
| 		}
 | |
| 	}
 | |
| 	template <typename tFn>
 | |
| 	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<Token>& 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<std::weak_ptr<Token>> m_tokens; // Mutable so we can remove expired tokens while converting bool
 | |
| };
 | |
| 
 | |
| NAMESPACE_SQUID_END
 | |
| 
 | |
| ///@} end of Tokens group
 | 
