#pragma once
#include <vector>
#include <memory>
#include <atomic>
#include <cmath> // needed for floor() used in json headers below (iOS)
#include "rapidjson/rapidjson.h"
#include "rapidjson/writer.h"
#include "RKJson.h"
#include "Utils/Json.h"
#include "Utils/Serializer.h"
#include "EventSystem/EventSystem.h"
#include <type_traits>
#define GA_BEGIN(name) \
class name : public GameActionBase<name> \
{ \
private: \
typedef name _this_type; \
public: \
name() : GameActionBase() {} \
name(SupressActionIdIncrement dummy) : GameActionBase(dummy) {} \
static const char* ActionType() { return #name; } \
typedef struct { \
template <typename V> \
static void Visit(_this_type* o, V& v) \
{ \
v.Visit("_i_", o->actionId); \
v.Visit("_r_", o->responseToId); \
}
// Internal macro not intended to be used directly
#define GA_MEMBER_BEGIN(name) \
/* This macro magic to chain members together in this fashion I call "Ryland's Device" */ \
} visit_ns_##name; \
// Internal macro not intended to be used directly
#define GA_MEMBER_END(name, func) \
/* This macro magic to chain members together in this fashion I call "Ryland's Device" */ \
typedef struct \
{ \
template <typename V> \
static void Visit(_this_type* o, V& v) \
{ \
visit_ns_##name::Visit(o, v); \
func \
}
// Usage: Without defaults: GA_MEMBER(int, m_val) or With a default value: GA_MEMBER(int, m_val, 5)
#define GA_MEMBER(type, name, ...) \
GA_MEMBER_BEGIN(name) \
type name = type(__VA_ARGS__); \
GA_MEMBER_END(name, v.Visit(#name, o->name); )
// Note, arrays are just std::vectors. Intended usage: GA_ARRAY_MEMBER(int, m_array) creates: std::vector<int> m_array;
#define GA_ARRAY_MEMBER(type, name) \
GA_MEMBER_BEGIN(name) \
std::vector<type> name; \
GA_MEMBER_END(name, v.Visit(#name, o->name); )
// Usage: GA_MEMBER_FUNCTION_BEGIN(int, myFunc, (int a, int b)) <function body> GA_MEMEBER_FUNCTION_END(myFunc)
#define GA_MEMBER_FUNCTION_BEGIN(returnType, name, parameters) \
GA_MEMBER_BEGIN(name) \
returnType name parameters \
{
// Usage: GA_MEMBER_FUNCTION_BEGIN(int, myFunc, (int a, int b)) <function body> GA_MEMEBER_FUNCTION_END(myFunc)
#define GA_MEMBER_FUNCTION_END(name) \
} \
GA_MEMBER_END(name, )
#define GA_END(name) \
GA_MEMBER_BEGIN(end_of_members) \
template <typename V> \
void Visit(V& v) \
{ \
v.Enter(ActionType()); \
visit_ns_end_of_members::Visit(this, v); \
v.Exit(ActionType()); \
} \
}; \
\
static struct register_##name \
{ \
/* daisy chain during program initialization the GameAction types in to a linked list */ \
register_##name() { static RegisterActionFactoryItem factory(#name, name::DeserializeAndDispatch); } \
} s_register_##name##_obj;
struct RegisterActionFactoryItem
{
// Iterate the action factories
static void ForEach(std::function<void(const RegisterActionFactoryItem& a_actionFactory)> a_function)
{
const RegisterActionFactoryItem* item = GetHead();
while (item)
{
a_function(*item);
item = &item->m_next;
}
}
RegisterActionFactoryItem(const char* a_name, void(*a_dispatch)(EventSink&, bne::JsonValueConstRef)) : m_name(a_name), m_dispatch(a_dispatch), m_next(*GetHead())
{
// Linked list
GetHead() = this;
}
const char* m_name;
void(*m_dispatch)(EventSink& a_eventSink, bne::JsonValueConstRef a_inData);
private:
const RegisterActionFactoryItem& m_next;
static RegisterActionFactoryItem*& GetHead();
};
inline RegisterActionFactoryItem*& RegisterActionFactoryItem::GetHead()
{
static RegisterActionFactoryItem* s_head = nullptr; // initialized to null - will happen when the first RegisterActionFactoryItem object is created
return s_head;
}
namespace details
{
// Internal function for the generation of the unique action id in an action
// action ids start at 1. An actionId of 0 is invalid
// This id is unique per instance of an action, not per action type.
uint32_t GetNextActionId();
// For unit testing only
void ResetGameActionIds();
}
class SupressActionIdIncrement
{
};
template <class ActionT>
struct GameActionBase : public EventBase<ActionT>
{
public:
uint32_t actionId;
uint32_t responseToId;
GameActionBase()
{
actionId = details::GetNextActionId();
responseToId = 0;
}
GameActionBase(SupressActionIdIncrement dummy)
{
actionId = 0;
responseToId = 0;
}
template <typename T>
void SetRespondingToAction(const GameActionBase<T>& a_otherAction)
{
responseToId = a_otherAction.actionId;
}
template <typename T>
bool IsResponseToAction(const GameActionBase<T>& a_otherAction) const
{
return responseToId == a_otherAction.actionId;
}
static void DeserializeAndDispatch(EventSink& a_eventSink, bne::JsonValueConstRef a_inData);
static bool SerializeToJson(ActionT& a_gameAction, std::vector<uint8_t>& a_outData);
};
// Use the new serialization system
class JsonSerializerVisitor
{
public:
JsonSerializerVisitor(JsonSerializerBase& a_serializer) : m_serializer(a_serializer) {}
inline void SetResult(bool okay) { if (!okay) m_error = true; }
inline bool GetError() const { return m_error; }
void Enter(const char* a_scopeName) { m_serializer.PushFrame(a_scopeName); }
void Exit(const char* a_scopeName) { m_serializer.PopFrame(); }
template <typename T, int>
struct EnableIfActionOrIntrinsic // Intrinsic case
{
EnableIfActionOrIntrinsic(JsonSerializerVisitor& v, const char* a_memberName, T& a_value)
{
v.SetResult(SerializerDispatch<T>::Process(a_value, a_memberName, v.m_serializer));
}
};
template <typename T>
struct EnableIfActionOrIntrinsic<T, 1> // GameAction case
{
EnableIfActionOrIntrinsic(JsonSerializerVisitor& v, const char* a_memberName, T& a_value)
{
a_value.Visit(v);
}
};
template <typename T>
void Visit(const char* a_memberName, T& a_value)
{
// Select code to call depending if T is derived from a GameAction or not
EnableIfActionOrIntrinsic<T, std::is_base_of< GameActionBase<T>, T >::value>(*this, a_memberName, a_value);
}
private:
JsonSerializerBase& m_serializer;
bool m_error = false;
};
template <class ActionT>
void GameActionBase<ActionT>::DeserializeAndDispatch(EventSink& a_eventSink, bne::JsonValueConstRef a_inData)
{
// TODO: Disabling this minor optimization for now. If event system can support this then we could put this back
//if (EventBase<ActionT>::HasListeners())
{
SupressActionIdIncrement dummy;
ActionT obj(dummy);
JsonReader reader(a_inData, nullptr, false); // #TODO OM: had to disable migration here to deal with non-standard use
// of the API. Should fix this properly later (although we probably still
// want to turn migration off for GA's)
JsonSerializerVisitor visitor(reader);
obj.Visit(visitor);
if (!visitor.GetError())
{
a_eventSink.Enqueue(std::move(obj));
}
}
return;
}
template <class ActionT>
bool GameActionBase<ActionT>::SerializeToJson(ActionT& a_gameAction, std::vector<uint8_t>& a_outData)
{
bne::JsonValue result;
JsonWriter writer(result);
JsonSerializerVisitor visitor(writer);
a_gameAction.Visit(visitor);
if (!visitor.GetError())
{
std::string str = result.toCompactString();
size_t oldSize = a_outData.size();
a_outData.resize(oldSize + str.size());
memcpy(a_outData.data() + oldSize, str.data(), str.size());
return true;
}
return false;
}
/*
Old ZA code for adding a single GameAction (approximately what it was like)
// Add it to the enum (GameActions.h?)
enum GameActions
{
GA_ProfileUpdate
};
// h (Profile.h)
GA_BEGIN(PropfileUpdateAction, GA_ProfileUpdate)
int m_blah;
GA_END()
//cpp (Profile.cpp)
GA_BEGIN_SERIALIZE(PropfileUpdateAction)
GA_SERIALIZE(int, m_blah)
GA_END_SERIALIZE()
// Register it (GameActions.cpp?)
void RegisterAction()
{
REGISTER_GA(GA_ProfileUpdate);
}
// Creating one (which also queues it)
PropfileUpdateAction* action = GAM->CreateAction<GA_ProfileUpdate>();
The GAM (GameActionManager) would serialize and de-serialize this via the SystemNetworkService
and then dispatch via the event system as an event. This event has a void* data payload which
made the event dispatch not so type safe.
Various problems during development included:
- not doing all the steps required to add a new GameAction correctly
- When adding one, long compile times after changing the enum
- Compatility issues between the client and server when making changes to the game actions (enum sent as type as int over the wire)
- GameDB change actions invalidating in-flight/queued actions. Also GameDB refs/pointers in code.
- Type safety of the event dispatch
- The 2 ways to create game actions (CreateAction<GA_ProfileUpdate> or CreateAction<PropfileUpdateAction::VALUE>) which made searching for actions harder
// Example new way:
// Only declare in one place (no need to put stuff in h and cpp files and keep in sync)
// No need to register the action
// There is no enum, based on string matching which can be optimized by use of hashing
// although a std::map with RB-tree will probably be plenty good enough
// Over the wire between client and server, use of a string instead of enum value as type
// will be safer anyway.
GA_BEGIN(Inventory)
GA_MEMBER(int32_t, m_coins) // specifying defaults is optional
GA_MEMBER(int32_t, m_gems, 0)
GA_MEMBER(float, m_blah, 9.0)
GA_END(Inventory)
GA_BEGIN(ProfileUpdate)
GA_MEMBER(Inventory, m_inventory)
GA_MEMBER(int32_t, m_val, 100)
GA_END(ProfileUpdate)
A new feature is setting an action as being in response to another action. This is useful
in that there can be explicit confirmation or building a chain of communication between
client and server where as in ZA we sometimes ran in to problems where we might wait for
a response, but not be sure if it was a response sent as a result of the action we sent
or something else that happened. Now it can be clear.
Example usage:
void GotActionHandler(const RequestActionType& a)
{
ResponseActionType b;
b.SetRespondingToAction(a);
...
}
Example usage of sending a GameAction:
GA_BEGIN(Ping)
GA_MEMBER(int32_t, m_variable, 0)
GA_END(Ping)
void SendPing()
{
Ping ping;
ping.m_variable = 1234;
GameServerInterface::Get()->QueueAction(ping);
}
*/