From e91b8cfcec9386d60ebd3e2eec46598582741092 Mon Sep 17 00:00:00 2001
From: Kawe Mazidjatari <48657826+Mauler125@users.noreply.github.com>
Date: Fri, 5 Apr 2024 18:39:36 +0200
Subject: [PATCH] RTech: implement custom events and slight
 adjustments/improvements

Implemented CustomEvent in code, which supports:
- bool|int|float|string|vector|array|table
- nested arrays and tables, up to a depth of 64

Also improved foundation code for LiveAPI:
- added ability to log liveapi events to a file on the disk (rotates between each match or round, depending on how the abstracted functions are called in scripts)
- when the system is enabled through cvars, code will be invoked on the fly
- when the system is disabled through cvars, the system will be shutdown properly on the fly (properly handling socket closing, log file finishing, etc)
- if the socket system is enabled/disabled on the fly using cvars, related code will be called to initiate or shutdown the connections.

The generated proto.cpp/h file has been moved to the protoc project as it was causing some compiler warnings that we suppress on the thirdparty (vendored) code.
---
 r5dev/core/CMakeLists.txt                     |   1 +
 r5dev/game/CMakeLists.txt                     |   2 -
 r5dev/game/server/liveapi/liveapi.cpp         | 241 ++++++-
 r5dev/protoc/CMakeLists.txt                   |  12 +
 .../server/liveapi => protoc}/events.pb.cc    | 594 ++++++++++--------
 .../server/liveapi => protoc}/events.pb.h     | 209 ++++--
 r5dev/resource/protobuf/events.proto          |  46 +-
 r5dev/rtech/liveapi/liveapi.cpp               | 193 +++++-
 r5dev/rtech/liveapi/liveapi.h                 |  26 +-
 9 files changed, 934 insertions(+), 390 deletions(-)
 rename r5dev/{game/server/liveapi => protoc}/events.pb.cc (97%)
 rename r5dev/{game/server/liveapi => protoc}/events.pb.h (99%)

diff --git a/r5dev/core/CMakeLists.txt b/r5dev/core/CMakeLists.txt
index bf387d5d..d8f575bf 100644
--- a/r5dev/core/CMakeLists.txt
+++ b/r5dev/core/CMakeLists.txt
@@ -58,6 +58,7 @@ target_link_libraries( ${PROJECT_NAME} PRIVATE
     "vphysics"
 
     "SigCache_Pb"
+    "LiveAPI_Pb"
     "SV_RCon_Pb"
     "CL_RCon_Pb"
 
diff --git a/r5dev/game/CMakeLists.txt b/r5dev/game/CMakeLists.txt
index 015b5d71..8eae5857 100644
--- a/r5dev/game/CMakeLists.txt
+++ b/r5dev/game/CMakeLists.txt
@@ -100,8 +100,6 @@ add_sources( SOURCE_GROUP "Utility"
 )
 
 add_sources( SOURCE_GROUP "LiveAPI"
-    "server/liveapi/events.pb.cc"
-    "server/liveapi/events.pb.h"
     "server/liveapi/liveapi.cpp"
     "server/liveapi/liveapi.h"
 )
diff --git a/r5dev/game/server/liveapi/liveapi.cpp b/r5dev/game/server/liveapi/liveapi.cpp
index b9e338c3..7ad96c52 100644
--- a/r5dev/game/server/liveapi/liveapi.cpp
+++ b/r5dev/game/server/liveapi/liveapi.cpp
@@ -8,7 +8,7 @@
 // - Add code callback for player weapon switched  ( event WeaponSwitched )
 //
 //=============================================================================//
-#include "tier1/utlsymbol.h"
+#include "tier1/depthcounter.h"
 #include "vscript/languages/squirrel_re/include/sqtable.h"
 #include "vscript/languages/squirrel_re/include/sqarray.h"
 #include "game/server/vscript_server.h"
@@ -16,8 +16,13 @@
 #include "liveapi.h"
 #include "engine/sys_utils.h"
 
-#include "events.pb.h"
-#include "protobuf/util/json_util.h"
+#pragma warning(push)
+#pragma warning(disable : 4505)
+#include "protoc/events.pb.h"
+#pragma warning(pop) 
+
+// The total nesting depth cannot exceed this number
+#define LIVEAPI_MAX_ITEM_DEPTH 64
 
 
 /*
@@ -50,6 +55,7 @@ static rtech::liveapi::CharacterSelected s_characterSelected;
 //static rtech::liveapi::CustomMatch_SetSettings s_customMatch_SetSettings;
 //static rtech::liveapi::CustomMatch_SetTeam s_customMatch_SetTeam;
 //static rtech::liveapi::CustomMatch_SetTeamName s_customMatch_SetTeamName;
+static rtech::liveapi::CustomEvent s_customEvent;
 static rtech::liveapi::GameStateChanged s_gameStateChanged;
 static rtech::liveapi::GibraltarShieldAbsorbed s_gibraltarShieldAbsorbed;
 static rtech::liveapi::GrenadeThrown s_grenadeThrown;
@@ -125,7 +131,7 @@ enum class eLiveAPI_EventTypes
 		//customMatch_SetSettings,
 		//customMatch_SetTeam,
 		//customMatch_SetTeamName,
-
+	customEvent,
 	datacenter,
 		//gameConVar,
 	gameStateChanged,
@@ -221,6 +227,7 @@ static const char* LiveAPI_EventTypeToString(const eLiveAPI_EventTypes eventType
 		//case eLiveAPI_EventTypes::customMatch_SetSettings: return "customMatch_SetSettings";
 		//case eLiveAPI_EventTypes::customMatch_SetTeam: return "customMatch_SetTeam";
 		//case eLiveAPI_EventTypes::customMatch_SetTeamName: return "customMatch_SetTeamName";
+	case eLiveAPI_EventTypes::customEvent: return "customEvent";
 	case eLiveAPI_EventTypes::datacenter: return "datacenter";
 		//case eLiveAPI_EventTypes::gameConVar: return "gameConVar";
 	case eLiveAPI_EventTypes::gameStateChanged: return "gameStateChanged";
@@ -319,9 +326,11 @@ static bool LiveAPI_CheckSwitchType(HSQUIRRELVM const v, const SQObjectPtr& obj)
 }
 
 #define LIVEAPI_ENSURE_TYPE(v, obj, expectType, eventMsg, fieldNum) { if (!LiveAPI_EnsureType(v, obj, expectType, eventMsg, fieldNum)) return false; }
-#define LIVEAPI_EMPTY_TABLE_ERROR(v, eventMsg) { v_SQVM_RaiseError(v, "Empty table on message \"%s\".", eventMsg->GetTypeName().c_str()); return false; }
+#define LIVEAPI_EMPTY_TABLE_ERROR(v, eventMsg) { v_SQVM_RaiseError(v, "Empty iterable on message \"%s\".", eventMsg->GetTypeName().c_str()); return false; }
 #define LIVEAPI_FIELD_ERROR(v, fieldNum, eventMsg) { v_SQVM_RaiseError(v, "Field \"%d\" doesn't exist in message \"%s\".", fieldNum, eventMsg->GetTypeName().c_str()); return false; }
 #define LIVEAPI_ONEOF_FIELD_ERROR(v, fieldNum, eventMsg) { v_SQVM_RaiseError(v, "Tried to set member \"%d\" of oneof field in message \"%s\" while another has already been set.", fieldNum, eventMsg->GetTypeName().c_str()); return false; }
+#define LIVEAPI_UNSUPPORTED_TYPE_ERROR(v, gotType, eventMsg) {v_SQVM_RaiseError(v, "Value type \"%s\" is not supported for message \"%s\".\n", IdType2Name(gotType), eventMsg->GetTypeName().c_str()); return false; }
+#define LIVEAPI_CHECK_RECURSION_DEPTH(v, currDepth) { if (currDepth > LIVEAPI_MAX_ITEM_DEPTH) { v_SQVM_RaiseError(v, "Exceeded nesting depth limit of \"%i\".", LIVEAPI_MAX_ITEM_DEPTH); return false; }}
 
 uint64_t GetUnixTimeStamp() // TODO: move elsewhere
 {
@@ -1743,6 +1752,185 @@ static bool LiveAPI_HandlePlayerStatChanged(HSQUIRRELVM const v, const SQObject&
 	return true;
 }
 
+static void LiveAPI_SetCustomVectorField(google::protobuf::Struct* const structData, const Vector3D* const vecData)
+{
+	google::protobuf::Map<std::string, google::protobuf::Value>* const fieldData = structData->mutable_fields();
+
+	(*fieldData)["x"].set_number_value(vecData->x);
+	(*fieldData)["y"].set_number_value(vecData->y);
+	(*fieldData)["z"].set_number_value(vecData->z);
+}
+
+static bool LiveAPI_SetCustomTableFields(HSQUIRRELVM const v, google::protobuf::Struct* const structData, const SQTable* const tableData);
+static bool LiveAPI_SetCustomArrayFields(HSQUIRRELVM const v, google::protobuf::ListValue* const listData, const SQArray* const arrayData);
+
+static int s_currentDepth = 0;
+
+static bool LiveAPI_SetCustomArrayFields(HSQUIRRELVM const v, google::protobuf::ListValue* const listData, const SQArray* const arrayData)
+{
+	CDepthCounter<int> counter(s_currentDepth);
+	bool ranLoop = false;
+
+	for (SQInteger i = 0; i < arrayData->Size(); i++)
+	{
+		const SQObject& valueObj = arrayData->_values[i];
+
+		if (sq_isnull(valueObj))
+			continue;
+
+		if (!ranLoop)
+			ranLoop = true;
+
+		const SQObjectType valueType = sq_type(valueObj);
+
+		switch (valueType)
+		{
+		case OT_BOOL:
+			listData->add_values()->set_bool_value(_bool(valueObj));
+			break;
+		case OT_INTEGER:
+			listData->add_values()->set_number_value(_integer(valueObj));
+			break;
+		case OT_FLOAT:
+			listData->add_values()->set_number_value(_float(valueObj));
+			break;
+		case OT_STRING:
+			listData->add_values()->set_string_value(_string(valueObj)->_val);
+			break;
+		case OT_VECTOR:
+			LiveAPI_SetCustomVectorField(listData->add_values()->mutable_struct_value(), _vector3d(valueObj));
+			break;
+		case OT_ARRAY:
+			LIVEAPI_CHECK_RECURSION_DEPTH(v, counter.Get());
+
+			if (arrayData == _array(valueObj))
+			{
+				v_SQVM_RaiseError(v, "Attempted to nest array \"%i\" into itself at index \"%i\".", counter.Get(), i);
+				return false;
+			}
+
+			if (!LiveAPI_SetCustomArrayFields(v, listData->add_values()->mutable_list_value(), _array(valueObj)))
+				return false;
+
+			break;
+		case OT_TABLE:
+			LIVEAPI_CHECK_RECURSION_DEPTH(v, counter.Get());
+
+			if (!LiveAPI_SetCustomTableFields(v, listData->add_values()->mutable_struct_value(), _table(valueObj)))
+				return false;
+
+			break;
+		default:
+			LIVEAPI_UNSUPPORTED_TYPE_ERROR(v, valueType, listData);
+		}
+	}
+
+	if (!ranLoop)
+		LIVEAPI_EMPTY_TABLE_ERROR(v, listData);
+
+	return true;
+}
+
+static bool LiveAPI_SetCustomTableFields(HSQUIRRELVM const v, google::protobuf::Struct* const structData, const SQTable* const tableData)
+{
+	CDepthCounter<int> counter(s_currentDepth);
+	bool ranLoop = false;
+
+	SQ_FOR_EACH_TABLE(tableData, i)
+	{
+		const SQTable::_HashNode& node = tableData->_nodes[i];
+
+		if (sq_isnull(node.key))
+			continue;
+
+		if (!ranLoop)
+			ranLoop = true;
+
+		const SQObjectType keyType = sq_type(node.key);
+
+		if (keyType != OT_STRING)
+		{
+			v_SQVM_RaiseError(v, "Key must be a \"%s\", got \"%s\" for message \"%s\" in table \"%i\" at index \"%i\".",
+				IdType2Name(OT_STRING), IdType2Name(keyType), structData->GetTypeName().c_str(), counter.Get(), i);
+
+			return false;
+		}
+
+		const SQObjectType valueType = sq_type(node.val);
+
+		switch (valueType)
+		{
+		case OT_BOOL:
+			(*structData->mutable_fields())[_string(node.key)->_val].set_bool_value(_bool(node.val));
+			break;
+		case OT_INTEGER:
+			(*structData->mutable_fields())[_string(node.key)->_val].set_number_value(_integer(node.val));
+			break;
+		case OT_FLOAT:
+			(*structData->mutable_fields())[_string(node.key)->_val].set_number_value(_float(node.val));
+			break;
+		case OT_STRING:
+			(*structData->mutable_fields())[_string(node.key)->_val].set_string_value(_string(node.val)->_val);
+			break;
+		case OT_VECTOR:
+			LiveAPI_SetCustomVectorField((*structData->mutable_fields())[_string(node.key)->_val].mutable_struct_value(), _vector3d(node.val));
+			break;
+		case OT_ARRAY:
+			LIVEAPI_CHECK_RECURSION_DEPTH(v, counter.Get());
+
+			if (!LiveAPI_SetCustomArrayFields(v, (*structData->mutable_fields())[_string(node.key)->_val].mutable_list_value(), _array(node.val)))
+				return false;
+
+			break;
+		case OT_TABLE:
+			LIVEAPI_CHECK_RECURSION_DEPTH(v, counter.Get());
+
+			if (tableData == _table(node.val))
+			{
+				v_SQVM_RaiseError(v, "Attempted to nest table \"%i\" into itself at index \"%i\".", counter.Get(), i);
+				return false;
+			}
+
+			if (!LiveAPI_SetCustomTableFields(v, (*structData->mutable_fields())[_string(node.key)->_val].mutable_struct_value(), _table(node.val)))
+				return false;
+
+			break;
+		default:
+			LIVEAPI_UNSUPPORTED_TYPE_ERROR(v, valueType, structData);
+		}
+	}
+
+	if (!ranLoop)
+		LIVEAPI_EMPTY_TABLE_ERROR(v, structData);
+
+	return true;
+}
+
+static bool LiveAPI_HandleCustomEvent(HSQUIRRELVM const v, const SQObject& obj, rtech::liveapi::CustomEvent* const event,
+	const eLiveAPI_EventTypes eventType, const SQInteger fieldNum)
+{
+	LiveAPI_SetCommonMessageFields(event, eventType);
+
+	switch (fieldNum)
+	{
+	case rtech::liveapi::CustomEvent::kNameFieldNumber:
+		LIVEAPI_ENSURE_TYPE(v, obj, OT_STRING, event, fieldNum);
+		event->set_name(_string(obj)->_val);
+
+		break;
+	case rtech::liveapi::CustomEvent::kDataFieldNumber:
+		LIVEAPI_ENSURE_TYPE(v, obj, OT_TABLE, event, fieldNum);
+		if (!LiveAPI_SetCustomTableFields(v, event->mutable_data(), _table(obj)))
+			return false;
+
+		break;
+	default:
+		LIVEAPI_FIELD_ERROR(v, fieldNum, event);
+	}
+
+	return true;
+}
+
 /*
 	███████╗██╗   ██╗███████╗███╗   ██╗████████╗  ██████╗ ██╗███████╗██████╗  █████╗ ████████╗ ██████╗██╗  ██╗███████╗██████╗ 
 	██╔════╝██║   ██║██╔════╝████╗  ██║╚══██╔══╝  ██╔══██╗██║██╔════╝██╔══██╗██╔══██╗╚══██╔══╝██╔════╝██║  ██║██╔════╝██╔══██╗
@@ -1757,19 +1945,7 @@ static void LiveAPI_SendEvent(const google::protobuf::Message* const msg)
 	s_liveAPIEvent.set_event_size(msg->ByteSize());
 	s_liveAPIEvent.mutable_gamemessage()->PackFrom(*msg);
 
-	const string data = s_liveAPIEvent.SerializeAsString();
-	LiveAPISystem()->LogEvent(data.c_str(), (int)data.size());
-
-	std::string jsonStr;
-
-	google::protobuf::util::JsonPrintOptions options;
-	options.add_whitespace = true;
-	options.always_print_primitive_fields = true;
-
-	google::protobuf::util::MessageToJsonString(s_liveAPIEvent, &jsonStr, options);
-
-	Msg(eDLL_T::ENGINE, "%s\n", jsonStr.c_str());
-
+	LiveAPISystem()->LogEvent(&s_liveAPIEvent, &s_liveAPIEvent.gamemessage());
 	s_liveAPIEvent.Clear();
 }
 
@@ -1818,6 +1994,10 @@ static bool LiveAPI_HandleEventByCategory(HSQUIRRELVM const v, const SQTable* co
 			msg = &s_bannerCollected;
 			ret = LiveAPI_HandleBannerCollected(v, obj, &s_bannerCollected, eventType, fieldNum);
 			break;
+		case eLiveAPI_EventTypes::customEvent:
+			msg = &s_customEvent;
+			ret = LiveAPI_HandleCustomEvent(v, obj, &s_customEvent, eventType, fieldNum);
+			break;
 		case eLiveAPI_EventTypes::inventoryPickUp:
 			msg = &s_inventoryPickUp;
 			ret = LiveAPI_HandleInventoryChange(v, obj, &s_inventoryPickUp, eventType, fieldNum);
@@ -1984,12 +2164,15 @@ namespace VScriptCode
 	{
 		SQRESULT LiveAPI_IsValidToRun(HSQUIRRELVM v);
 		SQRESULT LiveAPI_LogRaw(HSQUIRRELVM v);
+
+		SQRESULT LiveAPI_StartLogging(HSQUIRRELVM v);
+		SQRESULT LiveAPI_StopLogging(HSQUIRRELVM v);
 	}
 }
 
 SQRESULT VScriptCode::Server::LiveAPI_IsValidToRun(HSQUIRRELVM v)
 {
-	sq_pushbool(v, liveapi_enabled.GetBool());
+	sq_pushbool(v, LiveAPISystem()->IsValidToRun());
 	SCRIPT_CHECK_AND_RETURN(v, SQ_OK);
 }
 
@@ -1999,11 +2182,11 @@ SQRESULT VScriptCode::Server::LiveAPI_LogRaw(HSQUIRRELVM v)
 		SCRIPT_CHECK_AND_RETURN(v, SQ_OK);
 
 	SQRESULT result = SQ_OK;
-	SQObjectPtr& object = v->GetUp(-2);
+	const SQObjectPtr& object = v->GetUp(-2);
 
 	if (sq_istable(object))
 	{
-		SQTable* const table = object._unVal.pTable;
+		const SQTable* const table = object._unVal.pTable;
 		const eLiveAPI_EventTypes eventType = eLiveAPI_EventTypes(sq_getinteger(v, 2));
 
 		if (!LiveAPI_HandleEventByCategory(v, table, eventType))
@@ -2018,10 +2201,25 @@ SQRESULT VScriptCode::Server::LiveAPI_LogRaw(HSQUIRRELVM v)
 	SCRIPT_CHECK_AND_RETURN(v, result);
 }
 
+SQRESULT VScriptCode::Server::LiveAPI_StartLogging(HSQUIRRELVM v)
+{
+	LiveAPISystem()->CreateLogger();
+	SCRIPT_CHECK_AND_RETURN(v, SQ_OK);
+}
+
+SQRESULT VScriptCode::Server::LiveAPI_StopLogging(HSQUIRRELVM v)
+{
+	LiveAPISystem()->DestroyLogger();
+	SCRIPT_CHECK_AND_RETURN(v, SQ_OK);
+}
+
 void Script_RegisterLiveAPIFunctions(CSquirrelVM* const s)
 {
 	DEFINE_SERVER_SCRIPTFUNC_NAMED(s, LiveAPI_IsValidToRun, "Whether the LiveAPI system is enabled and able to run", "bool", "");
 	DEFINE_SERVER_SCRIPTFUNC_NAMED(s, LiveAPI_LogRaw, "VM bridge to the LiveAPI logger from scripts", "void", "table< int, var > data, int eventType");
+
+	DEFINE_SERVER_SCRIPTFUNC_NAMED(s, LiveAPI_StartLogging, "Start the LiveAPI session logger", "void", "");
+	DEFINE_SERVER_SCRIPTFUNC_NAMED(s, LiveAPI_StopLogging, "Stop the LiveAPI session logger", "void", "");
 }
 
 void Script_RegisterLiveAPIEnums(CSquirrelVM* const s)
@@ -2050,6 +2248,7 @@ void Script_RegisterLiveAPIEnums(CSquirrelVM* const s)
 			//"customMatch_SetSettings",
 			//"customMatch_SetTeam",
 			//"customMatch_SetTeamName",
+		"customEvent",
 		"datacenter",
 			//"gameConVar",
 		"gameStateChanged",
diff --git a/r5dev/protoc/CMakeLists.txt b/r5dev/protoc/CMakeLists.txt
index 88ff5c7f..8e78d772 100644
--- a/r5dev/protoc/CMakeLists.txt
+++ b/r5dev/protoc/CMakeLists.txt
@@ -1,4 +1,16 @@
 cmake_minimum_required( VERSION 3.16 )
+add_module( "lib" "LiveAPI_Pb" "vpc" ${FOLDER_CONTEXT} FALSE TRUE )
+
+start_sources()
+
+add_sources( SOURCE_GROUP "Runtime"
+    "events.pb.cc"
+    "events.pb.h"
+)
+
+end_sources()
+thirdparty_suppress_warnings()
+
 add_module( "lib" "SigCache_Pb" "vpc" ${FOLDER_CONTEXT} FALSE TRUE )
 
 start_sources()
diff --git a/r5dev/game/server/liveapi/events.pb.cc b/r5dev/protoc/events.pb.cc
similarity index 97%
rename from r5dev/game/server/liveapi/events.pb.cc
rename to r5dev/protoc/events.pb.cc
index fe2791ab..a64f40cb 100644
--- a/r5dev/game/server/liveapi/events.pb.cc
+++ b/r5dev/protoc/events.pb.cc
@@ -777,8 +777,9 @@ struct WeaponSwitchedDefaultTypeInternal {
 PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 WeaponSwitchedDefaultTypeInternal _WeaponSwitched_default_instance_;
 PROTOBUF_CONSTEXPR CustomEvent::CustomEvent(
     ::_pbi::ConstantInitialized): _impl_{
-    /*decltype(_impl_.customdata_)*/{}
-  , /*decltype(_impl_.category_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}}
+    /*decltype(_impl_.category_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}}
+  , /*decltype(_impl_.name_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}}
+  , /*decltype(_impl_.data_)*/nullptr
   , /*decltype(_impl_.timestamp_)*/uint64_t{0u}
   , /*decltype(_impl_._cached_size_)*/{}} {}
 struct CustomEventDefaultTypeInternal {
@@ -1531,7 +1532,8 @@ const uint32_t TableStruct_events_2eproto::offsets[] PROTOBUF_SECTION_VARIABLE(p
   ~0u,  // no _inlined_string_donated_
   PROTOBUF_FIELD_OFFSET(::rtech::liveapi::CustomEvent, _impl_.timestamp_),
   PROTOBUF_FIELD_OFFSET(::rtech::liveapi::CustomEvent, _impl_.category_),
-  PROTOBUF_FIELD_OFFSET(::rtech::liveapi::CustomEvent, _impl_.customdata_),
+  PROTOBUF_FIELD_OFFSET(::rtech::liveapi::CustomEvent, _impl_.name_),
+  PROTOBUF_FIELD_OFFSET(::rtech::liveapi::CustomEvent, _impl_.data_),
   ~0u,  // no _has_bits_
   PROTOBUF_FIELD_OFFSET(::rtech::liveapi::ChangeCamera, _internal_metadata_),
   ~0u,  // no _extensions_
@@ -1731,24 +1733,24 @@ static const ::_pbi::MigrationSchema schemas[] PROTOBUF_SECTION_VARIABLE(protode
   { 461, -1, -1, sizeof(::rtech::liveapi::AmmoUsed)},
   { 474, -1, -1, sizeof(::rtech::liveapi::WeaponSwitched)},
   { 485, -1, -1, sizeof(::rtech::liveapi::CustomEvent)},
-  { 494, -1, -1, sizeof(::rtech::liveapi::ChangeCamera)},
-  { 503, -1, -1, sizeof(::rtech::liveapi::PauseToggle)},
-  { 510, -1, -1, sizeof(::rtech::liveapi::CustomMatch_CreateLobby)},
-  { 516, -1, -1, sizeof(::rtech::liveapi::CustomMatch_JoinLobby)},
-  { 523, -1, -1, sizeof(::rtech::liveapi::CustomMatch_LeaveLobby)},
-  { 529, -1, -1, sizeof(::rtech::liveapi::CustomMatch_SetReady)},
-  { 536, -1, -1, sizeof(::rtech::liveapi::CustomMatch_GetLobbyPlayers)},
-  { 542, -1, -1, sizeof(::rtech::liveapi::CustomMatch_SetMatchmaking)},
-  { 549, -1, -1, sizeof(::rtech::liveapi::CustomMatch_SetTeam)},
-  { 558, -1, -1, sizeof(::rtech::liveapi::CustomMatch_KickPlayer)},
-  { 566, -1, -1, sizeof(::rtech::liveapi::CustomMatch_SetSettings)},
-  { 578, -1, -1, sizeof(::rtech::liveapi::CustomMatch_GetSettings)},
-  { 584, -1, -1, sizeof(::rtech::liveapi::CustomMatch_SetTeamName)},
-  { 592, -1, -1, sizeof(::rtech::liveapi::CustomMatch_SendChat)},
-  { 599, -1, -1, sizeof(::rtech::liveapi::Request)},
-  { 622, -1, -1, sizeof(::rtech::liveapi::RequestStatus)},
-  { 629, -1, -1, sizeof(::rtech::liveapi::Response)},
-  { 637, -1, -1, sizeof(::rtech::liveapi::LiveAPIEvent)},
+  { 495, -1, -1, sizeof(::rtech::liveapi::ChangeCamera)},
+  { 504, -1, -1, sizeof(::rtech::liveapi::PauseToggle)},
+  { 511, -1, -1, sizeof(::rtech::liveapi::CustomMatch_CreateLobby)},
+  { 517, -1, -1, sizeof(::rtech::liveapi::CustomMatch_JoinLobby)},
+  { 524, -1, -1, sizeof(::rtech::liveapi::CustomMatch_LeaveLobby)},
+  { 530, -1, -1, sizeof(::rtech::liveapi::CustomMatch_SetReady)},
+  { 537, -1, -1, sizeof(::rtech::liveapi::CustomMatch_GetLobbyPlayers)},
+  { 543, -1, -1, sizeof(::rtech::liveapi::CustomMatch_SetMatchmaking)},
+  { 550, -1, -1, sizeof(::rtech::liveapi::CustomMatch_SetTeam)},
+  { 559, -1, -1, sizeof(::rtech::liveapi::CustomMatch_KickPlayer)},
+  { 567, -1, -1, sizeof(::rtech::liveapi::CustomMatch_SetSettings)},
+  { 579, -1, -1, sizeof(::rtech::liveapi::CustomMatch_GetSettings)},
+  { 585, -1, -1, sizeof(::rtech::liveapi::CustomMatch_SetTeamName)},
+  { 593, -1, -1, sizeof(::rtech::liveapi::CustomMatch_SendChat)},
+  { 600, -1, -1, sizeof(::rtech::liveapi::Request)},
+  { 623, -1, -1, sizeof(::rtech::liveapi::RequestStatus)},
+  { 630, -1, -1, sizeof(::rtech::liveapi::Response)},
+  { 638, -1, -1, sizeof(::rtech::liveapi::LiveAPIEvent)},
 };
 
 static const ::_pb::Message* const file_default_instances[] = {
@@ -1819,224 +1821,227 @@ static const ::_pb::Message* const file_default_instances[] = {
 };
 
 const char descriptor_table_protodef_events_2eproto[] PROTOBUF_SECTION_VARIABLE(protodesc_cold) =
-  "\n\014events.proto\022\rrtech.liveapi\032\031google/pr"
-  "otobuf/any.proto\"*\n\007Vector3\022\t\n\001x\030\001 \001(\002\022\t"
-  "\n\001y\030\002 \001(\002\022\t\n\001z\030\003 \001(\002\"\276\002\n\006Player\022\014\n\004name\030"
-  "\001 \001(\t\022\016\n\006teamId\030\002 \001(\r\022#\n\003pos\030\003 \001(\0132\026.rte"
-  "ch.liveapi.Vector3\022&\n\006angles\030\004 \001(\0132\026.rte"
-  "ch.liveapi.Vector3\022\025\n\rcurrentHealth\030\005 \001("
-  "\r\022\021\n\tmaxHealth\030\006 \001(\r\022\024\n\014shieldHealth\030\007 \001"
-  "(\r\022\027\n\017shieldMaxHealth\030\010 \001(\r\022\023\n\013nucleusHa"
-  "sh\030\t \001(\t\022\024\n\014hardwareName\030\n \001(\t\022\020\n\010teamNa"
-  "me\030\013 \001(\t\022\022\n\nsquadIndex\030\014 \001(\r\022\021\n\tcharacte"
-  "r\030\r \001(\t\022\014\n\004skin\030\016 \001(\t\"b\n\027CustomMatch_Lob"
-  "byPlayer\022\014\n\004name\030\001 \001(\t\022\016\n\006teamId\030\002 \001(\r\022\023"
-  "\n\013nucleusHash\030\003 \001(\t\022\024\n\014hardwareName\030\004 \001("
-  "\t\"\?\n\nDatacenter\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010ca"
-  "tegory\030\002 \001(\t\022\014\n\004name\030\003 \001(\t\"V\n\007Version\022\021\n"
-  "\tmajor_num\030\001 \001(\r\022\021\n\tminor_num\030\002 \001(\r\022\023\n\013b"
-  "uild_stamp\030\003 \001(\r\022\020\n\010revision\030\004 \001(\t\"B\n\rIn"
-  "ventoryItem\022\020\n\010quantity\030\001 \001(\005\022\014\n\004item\030\002 "
-  "\001(\t\022\021\n\textraData\030\003 \001(\t\"v\n\024LoadoutConfigu"
-  "ration\022-\n\007weapons\030\001 \003(\0132\034.rtech.liveapi."
-  "InventoryItem\022/\n\tequipment\030\002 \003(\0132\034.rtech"
-  ".liveapi.InventoryItem\"\214\001\n\004Init\022\021\n\ttimes"
-  "tamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022\023\n\013gameVers"
-  "ion\030\003 \001(\t\022*\n\napiVersion\030\004 \001(\0132\026.rtech.li"
-  "veapi.Version\022\020\n\010platform\030\005 \001(\t\022\014\n\004name\030"
-  "\006 \001(\t\"h\n\030CustomMatch_LobbyPlayers\022\023\n\013pla"
-  "yerToken\030\001 \001(\t\0227\n\007players\030\002 \003(\0132&.rtech."
-  "liveapi.CustomMatch_LobbyPlayer\"\262\001\n\020Obse"
-  "rverSwitched\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010categ"
-  "ory\030\002 \001(\t\022\'\n\010observer\030\003 \001(\0132\025.rtech.live"
-  "api.Player\022%\n\006target\030\004 \001(\0132\025.rtech.livea"
-  "pi.Player\022)\n\ntargetTeam\030\005 \003(\0132\025.rtech.li"
-  "veapi.Player\"S\n\022ObserverAnnotation\022\021\n\tti"
-  "mestamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022\030\n\020annot"
-  "ationSerial\030\003 \001(\005\"\225\002\n\nMatchSetup\022\021\n\ttime"
-  "stamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022\013\n\003map\030\003 \001"
-  "(\t\022\024\n\014playlistName\030\004 \001(\t\022\024\n\014playlistDesc"
-  "\030\005 \001(\t\022-\n\ndatacenter\030\006 \001(\0132\031.rtech.livea"
-  "pi.Datacenter\022\023\n\013aimAssistOn\030\007 \001(\010\022\025\n\ran"
-  "onymousMode\030\010 \001(\010\022\020\n\010serverId\030\t \001(\t\022<\n\017s"
-  "tartingLoadout\030\n \001(\0132#.rtech.liveapi.Loa"
-  "doutConfiguration\"F\n\020GameStateChanged\022\021\n"
-  "\ttimestamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022\r\n\005st"
-  "ate\030\003 \001(\t\"_\n\021CharacterSelected\022\021\n\ttimest"
-  "amp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022%\n\006player\030\003 "
-  "\001(\0132\025.rtech.liveapi.Player\"k\n\rMatchState"
-  "End\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t"
-  "\022\r\n\005state\030\003 \001(\t\022&\n\007winners\030\004 \003(\0132\025.rtech"
-  ".liveapi.Player\"\260\001\n\020RingStartClosing\022\021\n\t"
-  "timestamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022\r\n\005sta"
-  "ge\030\003 \001(\r\022&\n\006center\030\004 \001(\0132\026.rtech.liveapi"
-  ".Vector3\022\025\n\rcurrentRadius\030\005 \001(\002\022\021\n\tendRa"
-  "dius\030\006 \001(\002\022\026\n\016shrinkDuration\030\007 \001(\002\"\240\001\n\023R"
-  "ingFinishedClosing\022\021\n\ttimestamp\030\001 \001(\004\022\020\n"
-  "\010category\030\002 \001(\t\022\r\n\005stage\030\003 \001(\r\022&\n\006center"
-  "\030\004 \001(\0132\026.rtech.liveapi.Vector3\022\025\n\rcurren"
-  "tRadius\030\005 \001(\002\022\026\n\016shrinkDuration\030\007 \001(\002\"]\n"
-  "\017PlayerConnected\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010c"
-  "ategory\030\002 \001(\t\022%\n\006player\030\003 \001(\0132\025.rtech.li"
-  "veapi.Player\"\207\001\n\022PlayerDisconnected\022\021\n\tt"
-  "imestamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022%\n\006play"
-  "er\030\003 \001(\0132\025.rtech.liveapi.Player\022\024\n\014canRe"
-  "connect\030\004 \001(\010\022\017\n\007isAlive\030\005 \001(\010\"\274\001\n\021Playe"
-  "rStatChanged\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010categ"
-  "ory\030\002 \001(\t\022%\n\006player\030\003 \001(\0132\025.rtech.liveap"
-  "i.Player\022\020\n\010statName\030\004 \001(\t\022\022\n\010intValue\030\005"
-  " \001(\rH\000\022\024\n\nfloatValue\030\006 \001(\002H\000\022\023\n\tboolValu"
-  "e\030\007 \001(\010H\000B\n\n\010newValue\"u\n\030PlayerUpgradeTi"
-  "erChanged\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010category"
-  "\030\002 \001(\t\022%\n\006player\030\003 \001(\0132\025.rtech.liveapi.P"
-  "layer\022\r\n\005level\030\004 \001(\005\"\255\001\n\rPlayerDamaged\022\021"
-  "\n\ttimestamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022\'\n\010a"
-  "ttacker\030\003 \001(\0132\025.rtech.liveapi.Player\022%\n\006"
-  "victim\030\004 \001(\0132\025.rtech.liveapi.Player\022\016\n\006w"
-  "eapon\030\005 \001(\t\022\027\n\017damageInflicted\030\006 \001(\r\"\275\001\n"
-  "\014PlayerKilled\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010cate"
-  "gory\030\002 \001(\t\022\'\n\010attacker\030\003 \001(\0132\025.rtech.liv"
-  "eapi.Player\022%\n\006victim\030\004 \001(\0132\025.rtech.live"
-  "api.Player\022(\n\tawardedTo\030\005 \001(\0132\025.rtech.li"
-  "veapi.Player\022\016\n\006weapon\030\006 \001(\t\"\223\001\n\014PlayerD"
-  "owned\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010category\030\002 \001"
-  "(\t\022\'\n\010attacker\030\003 \001(\0132\025.rtech.liveapi.Pla"
-  "yer\022%\n\006victim\030\004 \001(\0132\025.rtech.liveapi.Play"
-  "er\022\016\n\006weapon\030\005 \001(\t\"\224\001\n\014PlayerAssist\022\021\n\tt"
-  "imestamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022(\n\tassi"
-  "stant\030\003 \001(\0132\025.rtech.liveapi.Player\022%\n\006vi"
-  "ctim\030\004 \001(\0132\025.rtech.liveapi.Player\022\016\n\006wea"
-  "pon\030\005 \001(\t\"^\n\017SquadEliminated\022\021\n\ttimestam"
-  "p\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022&\n\007players\030\003 \003"
-  "(\0132\025.rtech.liveapi.Player\"\247\001\n\027GibraltarS"
-  "hieldAbsorbed\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010cate"
-  "gory\030\002 \001(\t\022\'\n\010attacker\030\003 \001(\0132\025.rtech.liv"
-  "eapi.Player\022%\n\006victim\030\004 \001(\0132\025.rtech.live"
-  "api.Player\022\027\n\017damageInflicted\030\006 \001(\r\"\253\001\n\033"
-  "RevenantForgedShadowDamaged\022\021\n\ttimestamp"
-  "\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022\'\n\010attacker\030\003 \001"
-  "(\0132\025.rtech.liveapi.Player\022%\n\006victim\030\004 \001("
-  "\0132\025.rtech.liveapi.Player\022\027\n\017damageInflic"
-  "ted\030\006 \001(\r\"\211\001\n\021PlayerRespawnTeam\022\021\n\ttimes"
-  "tamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022%\n\006player\030\003"
-  " \001(\0132\025.rtech.liveapi.Player\022(\n\trespawned"
-  "\030\004 \003(\0132\025.rtech.liveapi.Player\"\202\001\n\014Player"
-  "Revive\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010category\030\002 "
-  "\001(\t\022%\n\006player\030\003 \001(\0132\025.rtech.liveapi.Play"
-  "er\022&\n\007revived\030\004 \001(\0132\025.rtech.liveapi.Play"
-  "er\"\200\001\n\022ArenasItemSelected\022\021\n\ttimestamp\030\001"
-  " \001(\004\022\020\n\010category\030\002 \001(\t\022%\n\006player\030\003 \001(\0132\025"
-  ".rtech.liveapi.Player\022\014\n\004item\030\004 \001(\t\022\020\n\010q"
-  "uantity\030\005 \001(\005\"\202\001\n\024ArenasItemDeselected\022\021"
-  "\n\ttimestamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022%\n\006p"
-  "layer\030\003 \001(\0132\025.rtech.liveapi.Player\022\014\n\004it"
-  "em\030\004 \001(\t\022\020\n\010quantity\030\005 \001(\005\"}\n\017InventoryP"
-  "ickUp\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010category\030\002 \001"
-  "(\t\022%\n\006player\030\003 \001(\0132\025.rtech.liveapi.Playe"
-  "r\022\014\n\004item\030\004 \001(\t\022\020\n\010quantity\030\005 \001(\005\"\216\001\n\rIn"
-  "ventoryDrop\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010catego"
-  "ry\030\002 \001(\t\022%\n\006player\030\003 \001(\0132\025.rtech.liveapi"
-  ".Player\022\014\n\004item\030\004 \001(\t\022\020\n\010quantity\030\005 \001(\005\022"
-  "\021\n\textraData\030\006 \003(\t\"z\n\014InventoryUse\022\021\n\tti"
-  "mestamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022%\n\006playe"
-  "r\030\003 \001(\0132\025.rtech.liveapi.Player\022\014\n\004item\030\004"
-  " \001(\t\022\020\n\010quantity\030\005 \001(\005\"\207\001\n\017BannerCollect"
+  "\n\014events.proto\022\rrtech.liveapi\032\034google/pr"
+  "otobuf/struct.proto\032\031google/protobuf/any"
+  ".proto\"*\n\007Vector3\022\t\n\001x\030\001 \001(\002\022\t\n\001y\030\002 \001(\002\022"
+  "\t\n\001z\030\003 \001(\002\"\276\002\n\006Player\022\014\n\004name\030\001 \001(\t\022\016\n\006t"
+  "eamId\030\002 \001(\r\022#\n\003pos\030\003 \001(\0132\026.rtech.liveapi"
+  ".Vector3\022&\n\006angles\030\004 \001(\0132\026.rtech.liveapi"
+  ".Vector3\022\025\n\rcurrentHealth\030\005 \001(\r\022\021\n\tmaxHe"
+  "alth\030\006 \001(\r\022\024\n\014shieldHealth\030\007 \001(\r\022\027\n\017shie"
+  "ldMaxHealth\030\010 \001(\r\022\023\n\013nucleusHash\030\t \001(\t\022\024"
+  "\n\014hardwareName\030\n \001(\t\022\020\n\010teamName\030\013 \001(\t\022\022"
+  "\n\nsquadIndex\030\014 \001(\r\022\021\n\tcharacter\030\r \001(\t\022\014\n"
+  "\004skin\030\016 \001(\t\"b\n\027CustomMatch_LobbyPlayer\022\014"
+  "\n\004name\030\001 \001(\t\022\016\n\006teamId\030\002 \001(\r\022\023\n\013nucleusH"
+  "ash\030\003 \001(\t\022\024\n\014hardwareName\030\004 \001(\t\"\?\n\nDatac"
+  "enter\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010category\030\002 \001"
+  "(\t\022\014\n\004name\030\003 \001(\t\"V\n\007Version\022\021\n\tmajor_num"
+  "\030\001 \001(\r\022\021\n\tminor_num\030\002 \001(\r\022\023\n\013build_stamp"
+  "\030\003 \001(\r\022\020\n\010revision\030\004 \001(\t\"B\n\rInventoryIte"
+  "m\022\020\n\010quantity\030\001 \001(\005\022\014\n\004item\030\002 \001(\t\022\021\n\text"
+  "raData\030\003 \001(\t\"v\n\024LoadoutConfiguration\022-\n\007"
+  "weapons\030\001 \003(\0132\034.rtech.liveapi.InventoryI"
+  "tem\022/\n\tequipment\030\002 \003(\0132\034.rtech.liveapi.I"
+  "nventoryItem\"\214\001\n\004Init\022\021\n\ttimestamp\030\001 \001(\004"
+  "\022\020\n\010category\030\002 \001(\t\022\023\n\013gameVersion\030\003 \001(\t\022"
+  "*\n\napiVersion\030\004 \001(\0132\026.rtech.liveapi.Vers"
+  "ion\022\020\n\010platform\030\005 \001(\t\022\014\n\004name\030\006 \001(\t\"h\n\030C"
+  "ustomMatch_LobbyPlayers\022\023\n\013playerToken\030\001"
+  " \001(\t\0227\n\007players\030\002 \003(\0132&.rtech.liveapi.Cu"
+  "stomMatch_LobbyPlayer\"\262\001\n\020ObserverSwitch"
   "ed\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022"
-  "%\n\006player\030\003 \001(\0132\025.rtech.liveapi.Player\022("
-  "\n\tcollected\030\004 \001(\0132\025.rtech.liveapi.Player"
-  "\"u\n\021PlayerAbilityUsed\022\021\n\ttimestamp\030\001 \001(\004"
-  "\022\020\n\010category\030\002 \001(\t\022%\n\006player\030\003 \001(\0132\025.rte"
-  "ch.liveapi.Player\022\024\n\014linkedEntity\030\004 \001(\t\""
-  "\234\001\n\025LegendUpgradeSelected\022\021\n\ttimestamp\030\001"
+  "\'\n\010observer\030\003 \001(\0132\025.rtech.liveapi.Player"
+  "\022%\n\006target\030\004 \001(\0132\025.rtech.liveapi.Player\022"
+  ")\n\ntargetTeam\030\005 \003(\0132\025.rtech.liveapi.Play"
+  "er\"S\n\022ObserverAnnotation\022\021\n\ttimestamp\030\001 "
+  "\001(\004\022\020\n\010category\030\002 \001(\t\022\030\n\020annotationSeria"
+  "l\030\003 \001(\005\"\225\002\n\nMatchSetup\022\021\n\ttimestamp\030\001 \001("
+  "\004\022\020\n\010category\030\002 \001(\t\022\013\n\003map\030\003 \001(\t\022\024\n\014play"
+  "listName\030\004 \001(\t\022\024\n\014playlistDesc\030\005 \001(\t\022-\n\n"
+  "datacenter\030\006 \001(\0132\031.rtech.liveapi.Datacen"
+  "ter\022\023\n\013aimAssistOn\030\007 \001(\010\022\025\n\ranonymousMod"
+  "e\030\010 \001(\010\022\020\n\010serverId\030\t \001(\t\022<\n\017startingLoa"
+  "dout\030\n \001(\0132#.rtech.liveapi.LoadoutConfig"
+  "uration\"F\n\020GameStateChanged\022\021\n\ttimestamp"
+  "\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022\r\n\005state\030\003 \001(\t\""
+  "_\n\021CharacterSelected\022\021\n\ttimestamp\030\001 \001(\004\022"
+  "\020\n\010category\030\002 \001(\t\022%\n\006player\030\003 \001(\0132\025.rtec"
+  "h.liveapi.Player\"k\n\rMatchStateEnd\022\021\n\ttim"
+  "estamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022\r\n\005state\030"
+  "\003 \001(\t\022&\n\007winners\030\004 \003(\0132\025.rtech.liveapi.P"
+  "layer\"\260\001\n\020RingStartClosing\022\021\n\ttimestamp\030"
+  "\001 \001(\004\022\020\n\010category\030\002 \001(\t\022\r\n\005stage\030\003 \001(\r\022&"
+  "\n\006center\030\004 \001(\0132\026.rtech.liveapi.Vector3\022\025"
+  "\n\rcurrentRadius\030\005 \001(\002\022\021\n\tendRadius\030\006 \001(\002"
+  "\022\026\n\016shrinkDuration\030\007 \001(\002\"\240\001\n\023RingFinishe"
+  "dClosing\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010category\030"
+  "\002 \001(\t\022\r\n\005stage\030\003 \001(\r\022&\n\006center\030\004 \001(\0132\026.r"
+  "tech.liveapi.Vector3\022\025\n\rcurrentRadius\030\005 "
+  "\001(\002\022\026\n\016shrinkDuration\030\007 \001(\002\"]\n\017PlayerCon"
+  "nected\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010category\030\002 "
+  "\001(\t\022%\n\006player\030\003 \001(\0132\025.rtech.liveapi.Play"
+  "er\"\207\001\n\022PlayerDisconnected\022\021\n\ttimestamp\030\001"
   " \001(\004\022\020\n\010category\030\002 \001(\t\022%\n\006player\030\003 \001(\0132\025"
-  ".rtech.liveapi.Player\022\023\n\013upgradeName\030\004 \001"
-  "(\t\022\023\n\013upgradeDesc\030\005 \001(\t\022\r\n\005level\030\006 \001(\005\"o"
-  "\n\013ZiplineUsed\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010cate"
-  "gory\030\002 \001(\t\022%\n\006player\030\003 \001(\0132\025.rtech.livea"
-  "pi.Player\022\024\n\014linkedEntity\030\004 \001(\t\"q\n\rGrena"
-  "deThrown\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010category\030"
-  "\002 \001(\t\022%\n\006player\030\003 \001(\0132\025.rtech.liveapi.Pl"
-  "ayer\022\024\n\014linkedEntity\030\004 \001(\t\"m\n\021BlackMarke"
-  "tAction\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010category\030\002"
-  " \001(\t\022%\n\006player\030\003 \001(\0132\025.rtech.liveapi.Pla"
-  "yer\022\014\n\004item\030\004 \001(\t\"Z\n\014WraithPortal\022\021\n\ttim"
-  "estamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022%\n\006player"
-  "\030\003 \001(\0132\025.rtech.liveapi.Player\"Z\n\014WarpGat"
-  "eUsed\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010category\030\002 \001"
-  "(\t\022%\n\006player\030\003 \001(\0132\025.rtech.liveapi.Playe"
-  "r\"\250\001\n\010AmmoUsed\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010cat"
-  "egory\030\002 \001(\t\022%\n\006player\030\003 \001(\0132\025.rtech.live"
-  "api.Player\022\020\n\010ammoType\030\004 \001(\t\022\022\n\namountUs"
-  "ed\030\005 \001(\r\022\024\n\014oldAmmoCount\030\006 \001(\r\022\024\n\014newAmm"
-  "oCount\030\007 \001(\r\"\202\001\n\016WeaponSwitched\022\021\n\ttimes"
-  "tamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022%\n\006player\030\003"
-  " \001(\0132\025.rtech.liveapi.Player\022\021\n\toldWeapon"
-  "\030\004 \001(\t\022\021\n\tnewWeapon\030\005 \001(\t\"\\\n\013CustomEvent"
-  "\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022(\n"
-  "\ncustomData\030\003 \003(\0132\024.google.protobuf.Any\""
-  "X\n\014ChangeCamera\022.\n\003poi\030\001 \001(\0162\037.rtech.liv"
-  "eapi.PlayerOfInterestH\000\022\016\n\004name\030\002 \001(\tH\000B"
-  "\010\n\006target\"\037\n\013PauseToggle\022\020\n\010preTimer\030\001 \001"
-  "(\002\"\031\n\027CustomMatch_CreateLobby\"*\n\025CustomM"
-  "atch_JoinLobby\022\021\n\troleToken\030\001 \001(\t\"\030\n\026Cus"
-  "tomMatch_LeaveLobby\"\'\n\024CustomMatch_SetRe"
-  "ady\022\017\n\007isReady\030\001 \001(\010\"\035\n\033CustomMatch_GetL"
-  "obbyPlayers\"-\n\032CustomMatch_SetMatchmakin"
-  "g\022\017\n\007enabled\030\001 \001(\010\"\\\n\023CustomMatch_SetTea"
-  "m\022\016\n\006teamId\030\001 \001(\005\022\032\n\022targetHardwareName\030"
-  "\002 \001(\t\022\031\n\021targetNucleusHash\030\003 \001(\t\"O\n\026Cust"
-  "omMatch_KickPlayer\022\032\n\022targetHardwareName"
-  "\030\001 \001(\t\022\031\n\021targetNucleusHash\030\002 \001(\t\"\217\001\n\027Cu"
-  "stomMatch_SetSettings\022\024\n\014playlistName\030\001 "
-  "\001(\t\022\021\n\tadminChat\030\002 \001(\010\022\022\n\nteamRename\030\003 \001"
-  "(\010\022\022\n\nselfAssign\030\004 \001(\010\022\021\n\taimAssist\030\005 \001("
-  "\010\022\020\n\010anonMode\030\006 \001(\010\"\031\n\027CustomMatch_GetSe"
-  "ttings\";\n\027CustomMatch_SetTeamName\022\016\n\006tea"
-  "mId\030\001 \001(\005\022\020\n\010teamName\030\002 \001(\t\"$\n\024CustomMat"
-  "ch_SendChat\022\014\n\004text\030\001 \001(\t\"\226\010\n\007Request\022\017\n"
-  "\007withAck\030\001 \001(\010\022\024\n\014preSharedKey\030\002 \001(\t\0220\n\t"
-  "changeCam\030\004 \001(\0132\033.rtech.liveapi.ChangeCa"
-  "meraH\000\0221\n\013pauseToggle\030\005 \001(\0132\032.rtech.live"
-  "api.PauseToggleH\000\022I\n\027customMatch_CreateL"
-  "obby\030\n \001(\0132&.rtech.liveapi.CustomMatch_C"
-  "reateLobbyH\000\022E\n\025customMatch_JoinLobby\030\013 "
-  "\001(\0132$.rtech.liveapi.CustomMatch_JoinLobb"
-  "yH\000\022G\n\026customMatch_LeaveLobby\030\014 \001(\0132%.rt"
-  "ech.liveapi.CustomMatch_LeaveLobbyH\000\022C\n\024"
-  "customMatch_SetReady\030\r \001(\0132#.rtech.livea"
-  "pi.CustomMatch_SetReadyH\000\022O\n\032customMatch"
-  "_SetMatchmaking\030\016 \001(\0132).rtech.liveapi.Cu"
-  "stomMatch_SetMatchmakingH\000\022A\n\023customMatc"
-  "h_SetTeam\030\017 \001(\0132\".rtech.liveapi.CustomMa"
-  "tch_SetTeamH\000\022G\n\026customMatch_KickPlayer\030"
-  "\020 \001(\0132%.rtech.liveapi.CustomMatch_KickPl"
-  "ayerH\000\022I\n\027customMatch_SetSettings\030\021 \001(\0132"
-  "&.rtech.liveapi.CustomMatch_SetSettingsH"
-  "\000\022C\n\024customMatch_SendChat\030\022 \001(\0132#.rtech."
-  "liveapi.CustomMatch_SendChatH\000\022Q\n\033custom"
-  "Match_GetLobbyPlayers\030\023 \001(\0132*.rtech.live"
-  "api.CustomMatch_GetLobbyPlayersH\000\022I\n\027cus"
-  "tomMatch_SetTeamName\030\024 \001(\0132&.rtech.livea"
-  "pi.CustomMatch_SetTeamNameH\000\022I\n\027customMa"
-  "tch_GetSettings\030\025 \001(\0132&.rtech.liveapi.Cu"
-  "stomMatch_GetSettingsH\000B\t\n\007actions\"\037\n\rRe"
-  "questStatus\022\016\n\006status\030\001 \001(\t\"A\n\010Response\022"
-  "\017\n\007success\030\001 \001(\010\022$\n\006result\030\002 \001(\0132\024.googl"
-  "e.protobuf.Any\"M\n\014LiveAPIEvent\022\022\n\nevent_"
-  "size\030\001 \001(\007\022)\n\013gameMessage\030\003 \001(\0132\024.google"
-  ".protobuf.Any*\210\001\n\020PlayerOfInterest\022\017\n\013UN"
-  "SPECIFIED\020\000\022\010\n\004NEXT\020\001\022\014\n\010PREVIOUS\020\002\022\017\n\013K"
-  "ILL_LEADER\020\003\022\021\n\rCLOSEST_ENEMY\020\004\022\022\n\016CLOSE"
-  "ST_PLAYER\020\005\022\023\n\017LATEST_ATTACKER\020\006b\006proto3"
+  ".rtech.liveapi.Player\022\024\n\014canReconnect\030\004 "
+  "\001(\010\022\017\n\007isAlive\030\005 \001(\010\"\274\001\n\021PlayerStatChang"
+  "ed\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022"
+  "%\n\006player\030\003 \001(\0132\025.rtech.liveapi.Player\022\020"
+  "\n\010statName\030\004 \001(\t\022\022\n\010intValue\030\005 \001(\rH\000\022\024\n\n"
+  "floatValue\030\006 \001(\002H\000\022\023\n\tboolValue\030\007 \001(\010H\000B"
+  "\n\n\010newValue\"u\n\030PlayerUpgradeTierChanged\022"
+  "\021\n\ttimestamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022%\n\006"
+  "player\030\003 \001(\0132\025.rtech.liveapi.Player\022\r\n\005l"
+  "evel\030\004 \001(\005\"\255\001\n\rPlayerDamaged\022\021\n\ttimestam"
+  "p\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022\'\n\010attacker\030\003 "
+  "\001(\0132\025.rtech.liveapi.Player\022%\n\006victim\030\004 \001"
+  "(\0132\025.rtech.liveapi.Player\022\016\n\006weapon\030\005 \001("
+  "\t\022\027\n\017damageInflicted\030\006 \001(\r\"\275\001\n\014PlayerKil"
+  "led\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t"
+  "\022\'\n\010attacker\030\003 \001(\0132\025.rtech.liveapi.Playe"
+  "r\022%\n\006victim\030\004 \001(\0132\025.rtech.liveapi.Player"
+  "\022(\n\tawardedTo\030\005 \001(\0132\025.rtech.liveapi.Play"
+  "er\022\016\n\006weapon\030\006 \001(\t\"\223\001\n\014PlayerDowned\022\021\n\tt"
+  "imestamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022\'\n\010atta"
+  "cker\030\003 \001(\0132\025.rtech.liveapi.Player\022%\n\006vic"
+  "tim\030\004 \001(\0132\025.rtech.liveapi.Player\022\016\n\006weap"
+  "on\030\005 \001(\t\"\224\001\n\014PlayerAssist\022\021\n\ttimestamp\030\001"
+  " \001(\004\022\020\n\010category\030\002 \001(\t\022(\n\tassistant\030\003 \001("
+  "\0132\025.rtech.liveapi.Player\022%\n\006victim\030\004 \001(\013"
+  "2\025.rtech.liveapi.Player\022\016\n\006weapon\030\005 \001(\t\""
+  "^\n\017SquadEliminated\022\021\n\ttimestamp\030\001 \001(\004\022\020\n"
+  "\010category\030\002 \001(\t\022&\n\007players\030\003 \003(\0132\025.rtech"
+  ".liveapi.Player\"\247\001\n\027GibraltarShieldAbsor"
+  "bed\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t"
+  "\022\'\n\010attacker\030\003 \001(\0132\025.rtech.liveapi.Playe"
+  "r\022%\n\006victim\030\004 \001(\0132\025.rtech.liveapi.Player"
+  "\022\027\n\017damageInflicted\030\006 \001(\r\"\253\001\n\033RevenantFo"
+  "rgedShadowDamaged\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010"
+  "category\030\002 \001(\t\022\'\n\010attacker\030\003 \001(\0132\025.rtech"
+  ".liveapi.Player\022%\n\006victim\030\004 \001(\0132\025.rtech."
+  "liveapi.Player\022\027\n\017damageInflicted\030\006 \001(\r\""
+  "\211\001\n\021PlayerRespawnTeam\022\021\n\ttimestamp\030\001 \001(\004"
+  "\022\020\n\010category\030\002 \001(\t\022%\n\006player\030\003 \001(\0132\025.rte"
+  "ch.liveapi.Player\022(\n\trespawned\030\004 \003(\0132\025.r"
+  "tech.liveapi.Player\"\202\001\n\014PlayerRevive\022\021\n\t"
+  "timestamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022%\n\006pla"
+  "yer\030\003 \001(\0132\025.rtech.liveapi.Player\022&\n\007revi"
+  "ved\030\004 \001(\0132\025.rtech.liveapi.Player\"\200\001\n\022Are"
+  "nasItemSelected\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010ca"
+  "tegory\030\002 \001(\t\022%\n\006player\030\003 \001(\0132\025.rtech.liv"
+  "eapi.Player\022\014\n\004item\030\004 \001(\t\022\020\n\010quantity\030\005 "
+  "\001(\005\"\202\001\n\024ArenasItemDeselected\022\021\n\ttimestam"
+  "p\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022%\n\006player\030\003 \001("
+  "\0132\025.rtech.liveapi.Player\022\014\n\004item\030\004 \001(\t\022\020"
+  "\n\010quantity\030\005 \001(\005\"}\n\017InventoryPickUp\022\021\n\tt"
+  "imestamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022%\n\006play"
+  "er\030\003 \001(\0132\025.rtech.liveapi.Player\022\014\n\004item\030"
+  "\004 \001(\t\022\020\n\010quantity\030\005 \001(\005\"\216\001\n\rInventoryDro"
+  "p\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022%"
+  "\n\006player\030\003 \001(\0132\025.rtech.liveapi.Player\022\014\n"
+  "\004item\030\004 \001(\t\022\020\n\010quantity\030\005 \001(\005\022\021\n\textraDa"
+  "ta\030\006 \003(\t\"z\n\014InventoryUse\022\021\n\ttimestamp\030\001 "
+  "\001(\004\022\020\n\010category\030\002 \001(\t\022%\n\006player\030\003 \001(\0132\025."
+  "rtech.liveapi.Player\022\014\n\004item\030\004 \001(\t\022\020\n\010qu"
+  "antity\030\005 \001(\005\"\207\001\n\017BannerCollected\022\021\n\ttime"
+  "stamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022%\n\006player\030"
+  "\003 \001(\0132\025.rtech.liveapi.Player\022(\n\tcollecte"
+  "d\030\004 \001(\0132\025.rtech.liveapi.Player\"u\n\021Player"
+  "AbilityUsed\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010catego"
+  "ry\030\002 \001(\t\022%\n\006player\030\003 \001(\0132\025.rtech.liveapi"
+  ".Player\022\024\n\014linkedEntity\030\004 \001(\t\"\234\001\n\025Legend"
+  "UpgradeSelected\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010ca"
+  "tegory\030\002 \001(\t\022%\n\006player\030\003 \001(\0132\025.rtech.liv"
+  "eapi.Player\022\023\n\013upgradeName\030\004 \001(\t\022\023\n\013upgr"
+  "adeDesc\030\005 \001(\t\022\r\n\005level\030\006 \001(\005\"o\n\013ZiplineU"
+  "sed\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t"
+  "\022%\n\006player\030\003 \001(\0132\025.rtech.liveapi.Player\022"
+  "\024\n\014linkedEntity\030\004 \001(\t\"q\n\rGrenadeThrown\022\021"
+  "\n\ttimestamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022%\n\006p"
+  "layer\030\003 \001(\0132\025.rtech.liveapi.Player\022\024\n\014li"
+  "nkedEntity\030\004 \001(\t\"m\n\021BlackMarketAction\022\021\n"
+  "\ttimestamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022%\n\006pl"
+  "ayer\030\003 \001(\0132\025.rtech.liveapi.Player\022\014\n\004ite"
+  "m\030\004 \001(\t\"Z\n\014WraithPortal\022\021\n\ttimestamp\030\001 \001"
+  "(\004\022\020\n\010category\030\002 \001(\t\022%\n\006player\030\003 \001(\0132\025.r"
+  "tech.liveapi.Player\"Z\n\014WarpGateUsed\022\021\n\tt"
+  "imestamp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022%\n\006play"
+  "er\030\003 \001(\0132\025.rtech.liveapi.Player\"\250\001\n\010Ammo"
+  "Used\022\021\n\ttimestamp\030\001 \001(\004\022\020\n\010category\030\002 \001("
+  "\t\022%\n\006player\030\003 \001(\0132\025.rtech.liveapi.Player"
+  "\022\020\n\010ammoType\030\004 \001(\t\022\022\n\namountUsed\030\005 \001(\r\022\024"
+  "\n\014oldAmmoCount\030\006 \001(\r\022\024\n\014newAmmoCount\030\007 \001"
+  "(\r\"\202\001\n\016WeaponSwitched\022\021\n\ttimestamp\030\001 \001(\004"
+  "\022\020\n\010category\030\002 \001(\t\022%\n\006player\030\003 \001(\0132\025.rte"
+  "ch.liveapi.Player\022\021\n\toldWeapon\030\004 \001(\t\022\021\n\t"
+  "newWeapon\030\005 \001(\t\"g\n\013CustomEvent\022\021\n\ttimest"
+  "amp\030\001 \001(\004\022\020\n\010category\030\002 \001(\t\022\014\n\004name\030\003 \001("
+  "\t\022%\n\004data\030\004 \001(\0132\027.google.protobuf.Struct"
+  "\"X\n\014ChangeCamera\022.\n\003poi\030\001 \001(\0162\037.rtech.li"
+  "veapi.PlayerOfInterestH\000\022\016\n\004name\030\002 \001(\tH\000"
+  "B\010\n\006target\"\037\n\013PauseToggle\022\020\n\010preTimer\030\001 "
+  "\001(\002\"\031\n\027CustomMatch_CreateLobby\"*\n\025Custom"
+  "Match_JoinLobby\022\021\n\troleToken\030\001 \001(\t\"\030\n\026Cu"
+  "stomMatch_LeaveLobby\"\'\n\024CustomMatch_SetR"
+  "eady\022\017\n\007isReady\030\001 \001(\010\"\035\n\033CustomMatch_Get"
+  "LobbyPlayers\"-\n\032CustomMatch_SetMatchmaki"
+  "ng\022\017\n\007enabled\030\001 \001(\010\"\\\n\023CustomMatch_SetTe"
+  "am\022\016\n\006teamId\030\001 \001(\005\022\032\n\022targetHardwareName"
+  "\030\002 \001(\t\022\031\n\021targetNucleusHash\030\003 \001(\t\"O\n\026Cus"
+  "tomMatch_KickPlayer\022\032\n\022targetHardwareNam"
+  "e\030\001 \001(\t\022\031\n\021targetNucleusHash\030\002 \001(\t\"\217\001\n\027C"
+  "ustomMatch_SetSettings\022\024\n\014playlistName\030\001"
+  " \001(\t\022\021\n\tadminChat\030\002 \001(\010\022\022\n\nteamRename\030\003 "
+  "\001(\010\022\022\n\nselfAssign\030\004 \001(\010\022\021\n\taimAssist\030\005 \001"
+  "(\010\022\020\n\010anonMode\030\006 \001(\010\"\031\n\027CustomMatch_GetS"
+  "ettings\";\n\027CustomMatch_SetTeamName\022\016\n\006te"
+  "amId\030\001 \001(\005\022\020\n\010teamName\030\002 \001(\t\"$\n\024CustomMa"
+  "tch_SendChat\022\014\n\004text\030\001 \001(\t\"\226\010\n\007Request\022\017"
+  "\n\007withAck\030\001 \001(\010\022\024\n\014preSharedKey\030\002 \001(\t\0220\n"
+  "\tchangeCam\030\004 \001(\0132\033.rtech.liveapi.ChangeC"
+  "ameraH\000\0221\n\013pauseToggle\030\005 \001(\0132\032.rtech.liv"
+  "eapi.PauseToggleH\000\022I\n\027customMatch_Create"
+  "Lobby\030\n \001(\0132&.rtech.liveapi.CustomMatch_"
+  "CreateLobbyH\000\022E\n\025customMatch_JoinLobby\030\013"
+  " \001(\0132$.rtech.liveapi.CustomMatch_JoinLob"
+  "byH\000\022G\n\026customMatch_LeaveLobby\030\014 \001(\0132%.r"
+  "tech.liveapi.CustomMatch_LeaveLobbyH\000\022C\n"
+  "\024customMatch_SetReady\030\r \001(\0132#.rtech.live"
+  "api.CustomMatch_SetReadyH\000\022O\n\032customMatc"
+  "h_SetMatchmaking\030\016 \001(\0132).rtech.liveapi.C"
+  "ustomMatch_SetMatchmakingH\000\022A\n\023customMat"
+  "ch_SetTeam\030\017 \001(\0132\".rtech.liveapi.CustomM"
+  "atch_SetTeamH\000\022G\n\026customMatch_KickPlayer"
+  "\030\020 \001(\0132%.rtech.liveapi.CustomMatch_KickP"
+  "layerH\000\022I\n\027customMatch_SetSettings\030\021 \001(\013"
+  "2&.rtech.liveapi.CustomMatch_SetSettings"
+  "H\000\022C\n\024customMatch_SendChat\030\022 \001(\0132#.rtech"
+  ".liveapi.CustomMatch_SendChatH\000\022Q\n\033custo"
+  "mMatch_GetLobbyPlayers\030\023 \001(\0132*.rtech.liv"
+  "eapi.CustomMatch_GetLobbyPlayersH\000\022I\n\027cu"
+  "stomMatch_SetTeamName\030\024 \001(\0132&.rtech.live"
+  "api.CustomMatch_SetTeamNameH\000\022I\n\027customM"
+  "atch_GetSettings\030\025 \001(\0132&.rtech.liveapi.C"
+  "ustomMatch_GetSettingsH\000B\t\n\007actions\"\037\n\rR"
+  "equestStatus\022\016\n\006status\030\001 \001(\t\"A\n\010Response"
+  "\022\017\n\007success\030\001 \001(\010\022$\n\006result\030\002 \001(\0132\024.goog"
+  "le.protobuf.Any\"M\n\014LiveAPIEvent\022\022\n\nevent"
+  "_size\030\001 \001(\007\022)\n\013gameMessage\030\003 \001(\0132\024.googl"
+  "e.protobuf.Any*\210\001\n\020PlayerOfInterest\022\017\n\013U"
+  "NSPECIFIED\020\000\022\010\n\004NEXT\020\001\022\014\n\010PREVIOUS\020\002\022\017\n\013"
+  "KILL_LEADER\020\003\022\021\n\rCLOSEST_ENEMY\020\004\022\022\n\016CLOS"
+  "EST_PLAYER\020\005\022\023\n\017LATEST_ATTACKER\020\006b\006proto"
+  "3"
   ;
-static const ::_pbi::DescriptorTable* const descriptor_table_events_2eproto_deps[1] = {
+static const ::_pbi::DescriptorTable* const descriptor_table_events_2eproto_deps[2] = {
   &::descriptor_table_google_2fprotobuf_2fany_2eproto,
+  &::descriptor_table_google_2fprotobuf_2fstruct_2eproto,
 };
 static ::_pbi::once_flag descriptor_table_events_2eproto_once;
 const ::_pbi::DescriptorTable descriptor_table_events_2eproto = {
-    false, false, 8360, descriptor_table_protodef_events_2eproto,
+    false, false, 8401, descriptor_table_protodef_events_2eproto,
     "events.proto",
-    &descriptor_table_events_2eproto_once, descriptor_table_events_2eproto_deps, 1, 64,
+    &descriptor_table_events_2eproto_once, descriptor_table_events_2eproto_deps, 2, 64,
     schemas, file_default_instances, TableStruct_events_2eproto::offsets,
     file_level_metadata_events_2eproto, file_level_enum_descriptors_events_2eproto,
     file_level_service_descriptors_events_2eproto,
@@ -17582,10 +17587,18 @@ void WeaponSwitched::InternalSwap(WeaponSwitched* other) {
 
 class CustomEvent::_Internal {
  public:
+  static const ::PROTOBUF_NAMESPACE_ID::Struct& data(const CustomEvent* msg);
 };
 
-void CustomEvent::clear_customdata() {
-  _impl_.customdata_.Clear();
+const ::PROTOBUF_NAMESPACE_ID::Struct&
+CustomEvent::_Internal::data(const CustomEvent* msg) {
+  return *msg->_impl_.data_;
+}
+void CustomEvent::clear_data() {
+  if (GetArenaForAllocation() == nullptr && _impl_.data_ != nullptr) {
+    delete _impl_.data_;
+  }
+  _impl_.data_ = nullptr;
 }
 CustomEvent::CustomEvent(::PROTOBUF_NAMESPACE_ID::Arena* arena,
                          bool is_message_owned)
@@ -17597,8 +17610,9 @@ CustomEvent::CustomEvent(const CustomEvent& from)
   : ::PROTOBUF_NAMESPACE_ID::Message() {
   CustomEvent* const _this = this; (void)_this;
   new (&_impl_) Impl_{
-      decltype(_impl_.customdata_){from._impl_.customdata_}
-    , decltype(_impl_.category_){}
+      decltype(_impl_.category_){}
+    , decltype(_impl_.name_){}
+    , decltype(_impl_.data_){nullptr}
     , decltype(_impl_.timestamp_){}
     , /*decltype(_impl_._cached_size_)*/{}};
 
@@ -17611,6 +17625,17 @@ CustomEvent::CustomEvent(const CustomEvent& from)
     _this->_impl_.category_.Set(from._internal_category(), 
       _this->GetArenaForAllocation());
   }
+  _impl_.name_.InitDefault();
+  #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING
+    _impl_.name_.Set("", GetArenaForAllocation());
+  #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING
+  if (!from._internal_name().empty()) {
+    _this->_impl_.name_.Set(from._internal_name(), 
+      _this->GetArenaForAllocation());
+  }
+  if (from._internal_has_data()) {
+    _this->_impl_.data_ = new ::PROTOBUF_NAMESPACE_ID::Struct(*from._impl_.data_);
+  }
   _this->_impl_.timestamp_ = from._impl_.timestamp_;
   // @@protoc_insertion_point(copy_constructor:rtech.liveapi.CustomEvent)
 }
@@ -17620,8 +17645,9 @@ inline void CustomEvent::SharedCtor(
   (void)arena;
   (void)is_message_owned;
   new (&_impl_) Impl_{
-      decltype(_impl_.customdata_){arena}
-    , decltype(_impl_.category_){}
+      decltype(_impl_.category_){}
+    , decltype(_impl_.name_){}
+    , decltype(_impl_.data_){nullptr}
     , decltype(_impl_.timestamp_){uint64_t{0u}}
     , /*decltype(_impl_._cached_size_)*/{}
   };
@@ -17629,6 +17655,10 @@ inline void CustomEvent::SharedCtor(
   #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING
     _impl_.category_.Set("", GetArenaForAllocation());
   #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING
+  _impl_.name_.InitDefault();
+  #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING
+    _impl_.name_.Set("", GetArenaForAllocation());
+  #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING
 }
 
 CustomEvent::~CustomEvent() {
@@ -17642,8 +17672,9 @@ CustomEvent::~CustomEvent() {
 
 inline void CustomEvent::SharedDtor() {
   GOOGLE_DCHECK(GetArenaForAllocation() == nullptr);
-  _impl_.customdata_.~RepeatedPtrField();
   _impl_.category_.Destroy();
+  _impl_.name_.Destroy();
+  if (this != internal_default_instance()) delete _impl_.data_;
 }
 
 void CustomEvent::SetCachedSize(int size) const {
@@ -17656,8 +17687,12 @@ void CustomEvent::Clear() {
   // Prevent compiler warnings about cached_has_bits being unused
   (void) cached_has_bits;
 
-  _impl_.customdata_.Clear();
   _impl_.category_.ClearToEmpty();
+  _impl_.name_.ClearToEmpty();
+  if (GetArenaForAllocation() == nullptr && _impl_.data_ != nullptr) {
+    delete _impl_.data_;
+  }
+  _impl_.data_ = nullptr;
   _impl_.timestamp_ = uint64_t{0u};
   _internal_metadata_.Clear<::PROTOBUF_NAMESPACE_ID::UnknownFieldSet>();
 }
@@ -17686,16 +17721,21 @@ const char* CustomEvent::_InternalParse(const char* ptr, ::_pbi::ParseContext* c
         } else
           goto handle_unusual;
         continue;
-      // repeated .google.protobuf.Any customData = 3;
+      // string name = 3;
       case 3:
         if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 26)) {
-          ptr -= 1;
-          do {
-            ptr += 1;
-            ptr = ctx->ParseMessage(_internal_add_customdata(), ptr);
-            CHK_(ptr);
-            if (!ctx->DataAvailable(ptr)) break;
-          } while (::PROTOBUF_NAMESPACE_ID::internal::ExpectTag<26>(ptr));
+          auto str = _internal_mutable_name();
+          ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx);
+          CHK_(ptr);
+          CHK_(::_pbi::VerifyUTF8(str, "rtech.liveapi.CustomEvent.name"));
+        } else
+          goto handle_unusual;
+        continue;
+      // .google.protobuf.Struct data = 4;
+      case 4:
+        if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 34)) {
+          ptr = ctx->ParseMessage(_internal_mutable_data(), ptr);
+          CHK_(ptr);
         } else
           goto handle_unusual;
         continue;
@@ -17744,12 +17784,21 @@ uint8_t* CustomEvent::_InternalSerialize(
         2, this->_internal_category(), target);
   }
 
-  // repeated .google.protobuf.Any customData = 3;
-  for (unsigned i = 0,
-      n = static_cast<unsigned>(this->_internal_customdata_size()); i < n; i++) {
-    const auto& repfield = this->_internal_customdata(i);
+  // string name = 3;
+  if (!this->_internal_name().empty()) {
+    ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::VerifyUtf8String(
+      this->_internal_name().data(), static_cast<int>(this->_internal_name().length()),
+      ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::SERIALIZE,
+      "rtech.liveapi.CustomEvent.name");
+    target = stream->WriteStringMaybeAliased(
+        3, this->_internal_name(), target);
+  }
+
+  // .google.protobuf.Struct data = 4;
+  if (this->_internal_has_data()) {
     target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::
-        InternalWriteMessage(3, repfield, repfield.GetCachedSize(), target, stream);
+      InternalWriteMessage(4, _Internal::data(this),
+        _Internal::data(this).GetCachedSize(), target, stream);
   }
 
   if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) {
@@ -17768,13 +17817,6 @@ size_t CustomEvent::ByteSizeLong() const {
   // Prevent compiler warnings about cached_has_bits being unused
   (void) cached_has_bits;
 
-  // repeated .google.protobuf.Any customData = 3;
-  total_size += 1UL * this->_internal_customdata_size();
-  for (const auto& msg : this->_impl_.customdata_) {
-    total_size +=
-      ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize(msg);
-  }
-
   // string category = 2;
   if (!this->_internal_category().empty()) {
     total_size += 1 +
@@ -17782,6 +17824,20 @@ size_t CustomEvent::ByteSizeLong() const {
         this->_internal_category());
   }
 
+  // string name = 3;
+  if (!this->_internal_name().empty()) {
+    total_size += 1 +
+      ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize(
+        this->_internal_name());
+  }
+
+  // .google.protobuf.Struct data = 4;
+  if (this->_internal_has_data()) {
+    total_size += 1 +
+      ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize(
+        *_impl_.data_);
+  }
+
   // uint64 timestamp = 1;
   if (this->_internal_timestamp() != 0) {
     total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_timestamp());
@@ -17805,10 +17861,16 @@ void CustomEvent::MergeImpl(::PROTOBUF_NAMESPACE_ID::Message& to_msg, const ::PR
   uint32_t cached_has_bits = 0;
   (void) cached_has_bits;
 
-  _this->_impl_.customdata_.MergeFrom(from._impl_.customdata_);
   if (!from._internal_category().empty()) {
     _this->_internal_set_category(from._internal_category());
   }
+  if (!from._internal_name().empty()) {
+    _this->_internal_set_name(from._internal_name());
+  }
+  if (from._internal_has_data()) {
+    _this->_internal_mutable_data()->::PROTOBUF_NAMESPACE_ID::Struct::MergeFrom(
+        from._internal_data());
+  }
   if (from._internal_timestamp() != 0) {
     _this->_internal_set_timestamp(from._internal_timestamp());
   }
@@ -17831,12 +17893,20 @@ void CustomEvent::InternalSwap(CustomEvent* other) {
   auto* lhs_arena = GetArenaForAllocation();
   auto* rhs_arena = other->GetArenaForAllocation();
   _internal_metadata_.InternalSwap(&other->_internal_metadata_);
-  _impl_.customdata_.InternalSwap(&other->_impl_.customdata_);
   ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap(
       &_impl_.category_, lhs_arena,
       &other->_impl_.category_, rhs_arena
   );
-  swap(_impl_.timestamp_, other->_impl_.timestamp_);
+  ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap(
+      &_impl_.name_, lhs_arena,
+      &other->_impl_.name_, rhs_arena
+  );
+  ::PROTOBUF_NAMESPACE_ID::internal::memswap<
+      PROTOBUF_FIELD_OFFSET(CustomEvent, _impl_.timestamp_)
+      + sizeof(CustomEvent::_impl_.timestamp_)
+      - PROTOBUF_FIELD_OFFSET(CustomEvent, _impl_.data_)>(
+          reinterpret_cast<char*>(&_impl_.data_),
+          reinterpret_cast<char*>(&other->_impl_.data_));
 }
 
 ::PROTOBUF_NAMESPACE_ID::Metadata CustomEvent::GetMetadata() const {
diff --git a/r5dev/game/server/liveapi/events.pb.h b/r5dev/protoc/events.pb.h
similarity index 99%
rename from r5dev/game/server/liveapi/events.pb.h
rename to r5dev/protoc/events.pb.h
index 29844715..e59b7327 100644
--- a/r5dev/game/server/liveapi/events.pb.h
+++ b/r5dev/protoc/events.pb.h
@@ -32,6 +32,7 @@
 #include <thirdparty/protobuf/extension_set.h>  // IWYU pragma: export
 #include <thirdparty/protobuf/generated_enum_reflection.h>
 #include <thirdparty/protobuf/unknown_field_set.h>
+#include <thirdparty/protobuf/struct.pb.h>
 #include <thirdparty/protobuf/any.pb.h>
 // @@protoc_insertion_point(includes)
 #include <thirdparty/protobuf/port_def.inc>
@@ -9909,28 +9910,11 @@ class CustomEvent final :
   // accessors -------------------------------------------------------
 
   enum : int {
-    kCustomDataFieldNumber = 3,
     kCategoryFieldNumber = 2,
+    kNameFieldNumber = 3,
+    kDataFieldNumber = 4,
     kTimestampFieldNumber = 1,
   };
-  // repeated .google.protobuf.Any customData = 3;
-  int customdata_size() const;
-  private:
-  int _internal_customdata_size() const;
-  public:
-  void clear_customdata();
-  ::PROTOBUF_NAMESPACE_ID::Any* mutable_customdata(int index);
-  ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::PROTOBUF_NAMESPACE_ID::Any >*
-      mutable_customdata();
-  private:
-  const ::PROTOBUF_NAMESPACE_ID::Any& _internal_customdata(int index) const;
-  ::PROTOBUF_NAMESPACE_ID::Any* _internal_add_customdata();
-  public:
-  const ::PROTOBUF_NAMESPACE_ID::Any& customdata(int index) const;
-  ::PROTOBUF_NAMESPACE_ID::Any* add_customdata();
-  const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::PROTOBUF_NAMESPACE_ID::Any >&
-      customdata() const;
-
   // string category = 2;
   void clear_category();
   const std::string& category() const;
@@ -9945,6 +9929,38 @@ class CustomEvent final :
   std::string* _internal_mutable_category();
   public:
 
+  // string name = 3;
+  void clear_name();
+  const std::string& name() const;
+  template <typename ArgT0 = const std::string&, typename... ArgT>
+  void set_name(ArgT0&& arg0, ArgT... args);
+  std::string* mutable_name();
+  PROTOBUF_NODISCARD std::string* release_name();
+  void set_allocated_name(std::string* name);
+  private:
+  const std::string& _internal_name() const;
+  inline PROTOBUF_ALWAYS_INLINE void _internal_set_name(const std::string& value);
+  std::string* _internal_mutable_name();
+  public:
+
+  // .google.protobuf.Struct data = 4;
+  bool has_data() const;
+  private:
+  bool _internal_has_data() const;
+  public:
+  void clear_data();
+  const ::PROTOBUF_NAMESPACE_ID::Struct& data() const;
+  PROTOBUF_NODISCARD ::PROTOBUF_NAMESPACE_ID::Struct* release_data();
+  ::PROTOBUF_NAMESPACE_ID::Struct* mutable_data();
+  void set_allocated_data(::PROTOBUF_NAMESPACE_ID::Struct* data);
+  private:
+  const ::PROTOBUF_NAMESPACE_ID::Struct& _internal_data() const;
+  ::PROTOBUF_NAMESPACE_ID::Struct* _internal_mutable_data();
+  public:
+  void unsafe_arena_set_allocated_data(
+      ::PROTOBUF_NAMESPACE_ID::Struct* data);
+  ::PROTOBUF_NAMESPACE_ID::Struct* unsafe_arena_release_data();
+
   // uint64 timestamp = 1;
   void clear_timestamp();
   uint64_t timestamp() const;
@@ -9962,8 +9978,9 @@ class CustomEvent final :
   typedef void InternalArenaConstructable_;
   typedef void DestructorSkippable_;
   struct Impl_ {
-    ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::PROTOBUF_NAMESPACE_ID::Any > customdata_;
     ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr category_;
+    ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr name_;
+    ::PROTOBUF_NAMESPACE_ID::Struct* data_;
     uint64_t timestamp_;
     mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_;
   };
@@ -23499,41 +23516,139 @@ inline void CustomEvent::set_allocated_category(std::string* category) {
   // @@protoc_insertion_point(field_set_allocated:rtech.liveapi.CustomEvent.category)
 }
 
-// repeated .google.protobuf.Any customData = 3;
-inline int CustomEvent::_internal_customdata_size() const {
-  return _impl_.customdata_.size();
+// string name = 3;
+inline void CustomEvent::clear_name() {
+  _impl_.name_.ClearToEmpty();
 }
-inline int CustomEvent::customdata_size() const {
-  return _internal_customdata_size();
+inline const std::string& CustomEvent::name() const {
+  // @@protoc_insertion_point(field_get:rtech.liveapi.CustomEvent.name)
+  return _internal_name();
 }
-inline ::PROTOBUF_NAMESPACE_ID::Any* CustomEvent::mutable_customdata(int index) {
-  // @@protoc_insertion_point(field_mutable:rtech.liveapi.CustomEvent.customData)
-  return _impl_.customdata_.Mutable(index);
+template <typename ArgT0, typename... ArgT>
+inline PROTOBUF_ALWAYS_INLINE
+void CustomEvent::set_name(ArgT0&& arg0, ArgT... args) {
+ 
+ _impl_.name_.Set(static_cast<ArgT0 &&>(arg0), args..., GetArenaForAllocation());
+  // @@protoc_insertion_point(field_set:rtech.liveapi.CustomEvent.name)
 }
-inline ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::PROTOBUF_NAMESPACE_ID::Any >*
-CustomEvent::mutable_customdata() {
-  // @@protoc_insertion_point(field_mutable_list:rtech.liveapi.CustomEvent.customData)
-  return &_impl_.customdata_;
+inline std::string* CustomEvent::mutable_name() {
+  std::string* _s = _internal_mutable_name();
+  // @@protoc_insertion_point(field_mutable:rtech.liveapi.CustomEvent.name)
+  return _s;
 }
-inline const ::PROTOBUF_NAMESPACE_ID::Any& CustomEvent::_internal_customdata(int index) const {
-  return _impl_.customdata_.Get(index);
+inline const std::string& CustomEvent::_internal_name() const {
+  return _impl_.name_.Get();
 }
-inline const ::PROTOBUF_NAMESPACE_ID::Any& CustomEvent::customdata(int index) const {
-  // @@protoc_insertion_point(field_get:rtech.liveapi.CustomEvent.customData)
-  return _internal_customdata(index);
+inline void CustomEvent::_internal_set_name(const std::string& value) {
+  
+  _impl_.name_.Set(value, GetArenaForAllocation());
 }
-inline ::PROTOBUF_NAMESPACE_ID::Any* CustomEvent::_internal_add_customdata() {
-  return _impl_.customdata_.Add();
+inline std::string* CustomEvent::_internal_mutable_name() {
+  
+  return _impl_.name_.Mutable(GetArenaForAllocation());
 }
-inline ::PROTOBUF_NAMESPACE_ID::Any* CustomEvent::add_customdata() {
-  ::PROTOBUF_NAMESPACE_ID::Any* _add = _internal_add_customdata();
-  // @@protoc_insertion_point(field_add:rtech.liveapi.CustomEvent.customData)
-  return _add;
+inline std::string* CustomEvent::release_name() {
+  // @@protoc_insertion_point(field_release:rtech.liveapi.CustomEvent.name)
+  return _impl_.name_.Release();
 }
-inline const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::PROTOBUF_NAMESPACE_ID::Any >&
-CustomEvent::customdata() const {
-  // @@protoc_insertion_point(field_list:rtech.liveapi.CustomEvent.customData)
-  return _impl_.customdata_;
+inline void CustomEvent::set_allocated_name(std::string* name) {
+  if (name != nullptr) {
+    
+  } else {
+    
+  }
+  _impl_.name_.SetAllocated(name, GetArenaForAllocation());
+#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING
+  if (_impl_.name_.IsDefault()) {
+    _impl_.name_.Set("", GetArenaForAllocation());
+  }
+#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING
+  // @@protoc_insertion_point(field_set_allocated:rtech.liveapi.CustomEvent.name)
+}
+
+// .google.protobuf.Struct data = 4;
+inline bool CustomEvent::_internal_has_data() const {
+  return this != internal_default_instance() && _impl_.data_ != nullptr;
+}
+inline bool CustomEvent::has_data() const {
+  return _internal_has_data();
+}
+inline const ::PROTOBUF_NAMESPACE_ID::Struct& CustomEvent::_internal_data() const {
+  const ::PROTOBUF_NAMESPACE_ID::Struct* p = _impl_.data_;
+  return p != nullptr ? *p : reinterpret_cast<const ::PROTOBUF_NAMESPACE_ID::Struct&>(
+      ::PROTOBUF_NAMESPACE_ID::_Struct_default_instance_);
+}
+inline const ::PROTOBUF_NAMESPACE_ID::Struct& CustomEvent::data() const {
+  // @@protoc_insertion_point(field_get:rtech.liveapi.CustomEvent.data)
+  return _internal_data();
+}
+inline void CustomEvent::unsafe_arena_set_allocated_data(
+    ::PROTOBUF_NAMESPACE_ID::Struct* data) {
+  if (GetArenaForAllocation() == nullptr) {
+    delete reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(_impl_.data_);
+  }
+  _impl_.data_ = data;
+  if (data) {
+    
+  } else {
+    
+  }
+  // @@protoc_insertion_point(field_unsafe_arena_set_allocated:rtech.liveapi.CustomEvent.data)
+}
+inline ::PROTOBUF_NAMESPACE_ID::Struct* CustomEvent::release_data() {
+  
+  ::PROTOBUF_NAMESPACE_ID::Struct* temp = _impl_.data_;
+  _impl_.data_ = nullptr;
+#ifdef PROTOBUF_FORCE_COPY_IN_RELEASE
+  auto* old =  reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(temp);
+  temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp);
+  if (GetArenaForAllocation() == nullptr) { delete old; }
+#else  // PROTOBUF_FORCE_COPY_IN_RELEASE
+  if (GetArenaForAllocation() != nullptr) {
+    temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp);
+  }
+#endif  // !PROTOBUF_FORCE_COPY_IN_RELEASE
+  return temp;
+}
+inline ::PROTOBUF_NAMESPACE_ID::Struct* CustomEvent::unsafe_arena_release_data() {
+  // @@protoc_insertion_point(field_release:rtech.liveapi.CustomEvent.data)
+  
+  ::PROTOBUF_NAMESPACE_ID::Struct* temp = _impl_.data_;
+  _impl_.data_ = nullptr;
+  return temp;
+}
+inline ::PROTOBUF_NAMESPACE_ID::Struct* CustomEvent::_internal_mutable_data() {
+  
+  if (_impl_.data_ == nullptr) {
+    auto* p = CreateMaybeMessage<::PROTOBUF_NAMESPACE_ID::Struct>(GetArenaForAllocation());
+    _impl_.data_ = p;
+  }
+  return _impl_.data_;
+}
+inline ::PROTOBUF_NAMESPACE_ID::Struct* CustomEvent::mutable_data() {
+  ::PROTOBUF_NAMESPACE_ID::Struct* _msg = _internal_mutable_data();
+  // @@protoc_insertion_point(field_mutable:rtech.liveapi.CustomEvent.data)
+  return _msg;
+}
+inline void CustomEvent::set_allocated_data(::PROTOBUF_NAMESPACE_ID::Struct* data) {
+  ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation();
+  if (message_arena == nullptr) {
+    delete reinterpret_cast< ::PROTOBUF_NAMESPACE_ID::MessageLite*>(_impl_.data_);
+  }
+  if (data) {
+    ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena =
+        ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(
+                reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(data));
+    if (message_arena != submessage_arena) {
+      data = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage(
+          message_arena, data, submessage_arena);
+    }
+    
+  } else {
+    
+  }
+  _impl_.data_ = data;
+  // @@protoc_insertion_point(field_set_allocated:rtech.liveapi.CustomEvent.data)
 }
 
 // -------------------------------------------------------------------
diff --git a/r5dev/resource/protobuf/events.proto b/r5dev/resource/protobuf/events.proto
index 65acc2b7..d52d0231 100644
--- a/r5dev/resource/protobuf/events.proto
+++ b/r5dev/resource/protobuf/events.proto
@@ -25,31 +25,31 @@ message Vector3
 
 message Player
 {
-	string name = 1;
-	uint32 teamId = 2;
-	Vector3 pos = 3;
-	Vector3 angles = 4;
+	string name				= 1;
+	uint32 teamId			= 2;
+	Vector3 pos				= 3;
+	Vector3 angles			= 4;
 	
-	uint32 currentHealth = 5;
-	uint32 maxHealth = 6;
-	uint32 shieldHealth = 7;
-	uint32 shieldMaxHealth = 8;
+	uint32 currentHealth	= 5;
+	uint32 maxHealth		= 6;
+	uint32 shieldHealth		= 7;
+	uint32 shieldMaxHealth	= 8;
 	
-	string nucleusHash = 9;
-	string hardwareName = 10;
+	string nucleusHash		= 9;
+	string hardwareName		= 10;
 	
-	string teamName = 11;
-	uint32 squadIndex = 12;
-	string character = 13;
-	string skin = 14;
+	string teamName			= 11;
+	uint32 squadIndex		= 12;
+	string character		= 13;
+	string skin				= 14;
 }
 
 message CustomMatch_LobbyPlayer
 {
-	string name = 1;
-	uint32 teamId = 2;
+	string name			= 1;
+	uint32 teamId 		= 2;
 	
-	string nucleusHash = 3;
+	string nucleusHash 	= 3;
 	string hardwareName = 4;
 }
 
@@ -66,7 +66,7 @@ message Version
 	uint32 major_num	= 1;
 	uint32 minor_num	= 2;
 	uint32 build_stamp	= 3;
-	string revision = 4;
+	string revision 	= 4;
 }
 
 message InventoryItem
@@ -551,16 +551,18 @@ message WeaponSwitched
 // Custom events
 /////////////////////////////////////////
 
+import "google/protobuf/struct.proto";
+
 // Event defining custom user data that is otherwise too specific to create dedicated messages for
 message CustomEvent
 {
-	uint64 timestamp	= 1;
-	string category		= 2;
+	uint64 timestamp			= 1;
+	string category				= 2;
 	
-	repeated google.protobuf.Any customData = 3;
+	string name					= 3;
+	google.protobuf.Struct data	= 4;
 }
 
-
 //////////////////////////////////////////////////////////////////////
 // Input messages:
 // Used by observers to programmatically interact with the game
diff --git a/r5dev/rtech/liveapi/liveapi.cpp b/r5dev/rtech/liveapi/liveapi.cpp
index e48b49cd..6bf5d4ce 100644
--- a/r5dev/rtech/liveapi/liveapi.cpp
+++ b/r5dev/rtech/liveapi/liveapi.cpp
@@ -4,6 +4,7 @@
 // 
 //===========================================================================//
 #include "liveapi.h"
+#include "protobuf/util/json_util.h"
 
 #include "DirtySDK/dirtysock.h"
 #include "DirtySDK/dirtysock/netconn.h"
@@ -11,29 +12,33 @@
 #include "DirtySDK/proto/protowebsocket.h"
 
 //-----------------------------------------------------------------------------
-// 
+// change callbacks
 //-----------------------------------------------------------------------------
+static void LiveAPI_EnabledChangedCallback(IConVar* var, const char* pOldValue)
+{
+	LiveAPISystem()->ToggleInit();
+}
+static void LiveAPI_WebSocketEnabledChangedCallback(IConVar* var, const char* pOldValue)
+{
+	LiveAPISystem()->ToggleInitWebSocket();
+}
 static void LiveAPI_ParamsChangedCallback(IConVar* var, const char* pOldValue)
 {
 	LiveAPISystem()->UpdateParams();
 }
-
-//-----------------------------------------------------------------------------
-// 
-//-----------------------------------------------------------------------------
 static void LiveAPI_AddressChangedCallback(IConVar* var, const char* pOldValue)
 {
-	LiveAPISystem()->InstallAddressList();
+	LiveAPISystem()->RebootWebSocket();
 }
 
 //-----------------------------------------------------------------------------
 // console variables
 //-----------------------------------------------------------------------------
-ConVar liveapi_enabled("liveapi_enabled", "1", FCVAR_RELEASE | FCVAR_SERVER_FRAME_THREAD, "Enable LiveAPI functionality");
+ConVar liveapi_enabled("liveapi_enabled", "1", FCVAR_RELEASE | FCVAR_SERVER_FRAME_THREAD, "Enable LiveAPI functionality", &LiveAPI_EnabledChangedCallback);
 ConVar liveapi_session_name("liveapi_session_name", "liveapi_session", FCVAR_RELEASE | FCVAR_SERVER_FRAME_THREAD, "LiveAPI session name to identify this connection");
 
 // WebSocket core
-static ConVar liveapi_use_websocket("liveapi_use_websocket", "1", FCVAR_RELEASE | FCVAR_SERVER_FRAME_THREAD, "Use WebSocket to transmit LiveAPI events");
+static ConVar liveapi_websocket_enabled("liveapi_websocket_enabled", "1", FCVAR_RELEASE | FCVAR_SERVER_FRAME_THREAD, "Whether to use WebSocket to transmit LiveAPI events", &LiveAPI_WebSocketEnabledChangedCallback);
 static ConVar liveapi_servers("liveapi_servers", "ws://127.0.0.1:7777", FCVAR_RELEASE | FCVAR_SERVER_FRAME_THREAD, "Comma separated list of addresses to connect to", &LiveAPI_AddressChangedCallback, "ws://domain.suffix:port");
 
 // WebSocket connection base parameters
@@ -45,10 +50,23 @@ static ConVar liveapi_timeout("liveapi_timeout", "300", FCVAR_RELEASE | FCVAR_SE
 static ConVar liveapi_keepalive("liveapi_keepalive", "30", FCVAR_RELEASE | FCVAR_SERVER_FRAME_THREAD, "Interval of time to send Pong to any connected server", &LiveAPI_ParamsChangedCallback);
 static ConVar liveapi_lax_ssl("liveapi_lax_ssl", "1", FCVAR_RELEASE | FCVAR_SERVER_FRAME_THREAD, "Skip SSL certificate validation for all WSS connections (allows the use of self-signed certificates)", &LiveAPI_ParamsChangedCallback);
 
+// Print core
+static ConVar liveapi_print_enabled("liveapi_print_enabled", "0", FCVAR_RELEASE | FCVAR_SERVER_FRAME_THREAD, "Whether to enable the printing of all events to a LiveAPI JSON file");
+
+// Print parameters
+static ConVar liveapi_print_pretty("liveapi_print_pretty", "0", FCVAR_RELEASE | FCVAR_SERVER_FRAME_THREAD, "Whether to print events in a formatted manner to the LiveAPI JSON file");
+static ConVar liveapi_print_primitive("liveapi_print_primitive", "0", FCVAR_RELEASE | FCVAR_SERVER_FRAME_THREAD, "Whether to print primitive event fields to the LiveAPI JSON file");
+
 //-----------------------------------------------------------------------------
 // constructors/destructors
 //-----------------------------------------------------------------------------
 LiveAPI::LiveAPI()
+{
+	matchLogCount = 0;
+	initialLog = false;
+	initialized = false;
+}
+LiveAPI::~LiveAPI()
 {
 }
 
@@ -60,16 +78,8 @@ void LiveAPI::Init()
 	if (!liveapi_enabled.GetBool())
 		return;
 
-	if (liveapi_use_websocket.GetBool())
-	{
-		const char* initError = nullptr;
-
-		if (!InitWebSocket(initError))
-		{
-			Error(eDLL_T::RTECH, 0, "LiveAPI: WebSocket initialization failed! [%s]\n", initError);
-			return;
-		}
-	}
+	InitWebSocket();
+	initialized = true;
 }
 
 //-----------------------------------------------------------------------------
@@ -78,10 +88,26 @@ void LiveAPI::Init()
 void LiveAPI::Shutdown()
 {
 	webSocketSystem.Shutdown();
+	DestroyLogger();
+	initialized = false;
 }
 
 //-----------------------------------------------------------------------------
-// 
+// Toggle between init or deinit depending on current init state and the value
+// of the cvar 'liveapi_enabled'
+//-----------------------------------------------------------------------------
+void LiveAPI::ToggleInit()
+{
+	const bool enabled = liveapi_enabled.GetBool();
+
+	if (enabled && !initialized)
+		Init();
+	else if (!enabled && initialized)
+		Shutdown();
+}
+
+//-----------------------------------------------------------------------------
+// Populate the connection params structure
 //-----------------------------------------------------------------------------
 void LiveAPI::CreateParams(CWebSocket::ConnParams_s& params)
 {
@@ -96,7 +122,7 @@ void LiveAPI::CreateParams(CWebSocket::ConnParams_s& params)
 }
 
 //-----------------------------------------------------------------------------
-// 
+// Update the websocket parameters and apply them on all connections
 //-----------------------------------------------------------------------------
 void LiveAPI::UpdateParams()
 {
@@ -107,23 +133,91 @@ void LiveAPI::UpdateParams()
 }
 
 //-----------------------------------------------------------------------------
-// 
+// Initialize the websocket system
 //-----------------------------------------------------------------------------
-bool LiveAPI::InitWebSocket(const char*& initError)
+void LiveAPI::InitWebSocket()
 {
+	if (!liveapi_websocket_enabled.GetBool())
+		return;
+
 	CWebSocket::ConnParams_s connParams;
 	CreateParams(connParams);
 
-	return webSocketSystem.Init(liveapi_servers.GetString(), connParams, initError);
+	const char* initError = nullptr;
+
+	if (!webSocketSystem.Init(liveapi_servers.GetString(), connParams, initError))
+	{
+		Error(eDLL_T::RTECH, 0, "LiveAPI: WebSocket initialization failed! [%s]\n", initError);
+		return;
+	}
 }
 
 //-----------------------------------------------------------------------------
-// 
+// Shutdown the websocket system
 //-----------------------------------------------------------------------------
-void LiveAPI::InstallAddressList()
+void LiveAPI::ShutdownWebSocket()
 {
-	webSocketSystem.ClearAll();
-	webSocketSystem.UpdateAddressList(liveapi_servers.GetString());
+	webSocketSystem.Shutdown();
+}
+
+//-----------------------------------------------------------------------------
+// Toggle between init or deinit depending on current init state and the value
+// of the cvar 'liveapi_websocket_enabled'
+//-----------------------------------------------------------------------------
+void LiveAPI::ToggleInitWebSocket()
+{
+	const bool enabled = liveapi_websocket_enabled.GetBool();
+
+	if (enabled && !WebSocketInitialized())
+		InitWebSocket();
+	else if (!enabled && WebSocketInitialized())
+		ShutdownWebSocket();
+}
+
+//-----------------------------------------------------------------------------
+// Reboot the websocket system and reconnect to addresses specified in cvar
+// 'liveapi_servers'
+//-----------------------------------------------------------------------------
+void LiveAPI::RebootWebSocket()
+{
+	ShutdownWebSocket();
+	InitWebSocket();
+}
+
+//-----------------------------------------------------------------------------
+// Create the file logger
+//-----------------------------------------------------------------------------
+void LiveAPI::CreateLogger()
+{
+	// Its possible that one was already created but never closed, this is
+	// possible if the game scripts crashed or something along those lines.
+	DestroyLogger();
+
+	if (!liveapi_print_enabled.GetBool())
+		return; // Logging is disabled
+
+	matchLogger = spdlog::basic_logger_mt("match_logger",
+		Format("platform/liveapi/logs/%s/match_%d.json", g_LogSessionUUID.c_str(), matchLogCount++));
+
+	matchLogger.get()->set_pattern("%v");
+	matchLogger.get()->info("[\n");
+}
+
+//-----------------------------------------------------------------------------
+// Destroy the file logger
+//-----------------------------------------------------------------------------
+void LiveAPI::DestroyLogger()
+{
+	if (initialLog)
+		initialLog = false;
+
+	if (!matchLogger)
+		return; // Nothing to drop
+
+	matchLogger.get()->info("\n]\n");
+	matchLogger.reset();
+
+	spdlog::drop("match_logger");
 }
 
 //-----------------------------------------------------------------------------
@@ -134,30 +228,65 @@ void LiveAPI::RunFrame()
 	if (!IsEnabled())
 		return;
 
-	if (liveapi_use_websocket.GetBool())
+	if (WebSocketInitialized())
 		webSocketSystem.Update();
 }
 
 //-----------------------------------------------------------------------------
 // Send an event to all sockets
 //-----------------------------------------------------------------------------
-void LiveAPI::LogEvent(const char* const dataBuf, const int32_t dataSize)
+void LiveAPI::LogEvent(const google::protobuf::Message* const toTransmit, const google::protobuf::Message* toPrint)
 {
 	if (!IsEnabled())
 		return;
 
-	if (liveapi_use_websocket.GetBool())
-		webSocketSystem.SendData(dataBuf, dataSize);
+	if (WebSocketInitialized())
+	{
+		const string data = toTransmit->SerializeAsString();
+		webSocketSystem.SendData(data.c_str(), (int)data.size());
+	}
+
+	// NOTE: we don't check on the cvar 'liveapi_print_enabled' here because if
+	// this cvar gets disabled on the fly and we check it here, the output will
+	// be truncated and thus invalid! Log for as long as the SpdLog instance is
+	// valid.
+	if (matchLogger)
+	{
+		std::string jsonStr(initialLog ? ",\n" : "");
+		google::protobuf::util::JsonPrintOptions options;
+
+		options.add_whitespace = liveapi_print_pretty.GetBool();
+		options.always_print_primitive_fields = liveapi_print_primitive.GetBool();
+
+		google::protobuf::util::MessageToJsonString(*toPrint, &jsonStr, options);
+
+		// Remove the trailing newline character
+		if (options.add_whitespace && !jsonStr.empty())
+			jsonStr.pop_back();
+
+		matchLogger.get()->info(jsonStr);
+
+		if (!initialLog)
+			initialLog = true;
+	}
 }
 
 //-----------------------------------------------------------------------------
-// Returns whether the system is enabled and able to run
+// Returns whether the system is enabled
 //-----------------------------------------------------------------------------
 bool LiveAPI::IsEnabled() const
 {
 	return liveapi_enabled.GetBool();
 }
 
+//-----------------------------------------------------------------------------
+// Returns whether the system is able to run
+//-----------------------------------------------------------------------------
+bool LiveAPI::IsValidToRun() const
+{
+	return (IsEnabled() && (WebSocketInitialized() || FileLoggerInitialized()));
+}
+
 static LiveAPI s_liveApi;
 
 //-----------------------------------------------------------------------------
diff --git a/r5dev/rtech/liveapi/liveapi.h b/r5dev/rtech/liveapi/liveapi.h
index 17ffc5e8..ebd8e2dd 100644
--- a/r5dev/rtech/liveapi/liveapi.h
+++ b/r5dev/rtech/liveapi/liveapi.h
@@ -1,6 +1,7 @@
 #ifndef RTECH_LIVEAPI_H
 #define RTECH_LIVEAPI_H
 #include "tier2/websocket.h"
+#include "thirdparty/protobuf/message.h"
 
 #define LIVE_API_MAX_FRAME_BUFFER_SIZE 0x8000
 
@@ -13,26 +14,43 @@ typedef void (*LiveAPISendCallback_t)(ProtoWebSocketRefT* webSocket);
 class LiveAPI
 {
 public:
-
 	LiveAPI();
+	~LiveAPI();
 
 	void Init();
 	void Shutdown();
 
+	void ToggleInit();
+
 	void CreateParams(CWebSocket::ConnParams_s& params);
 	void UpdateParams();
 
-	bool InitWebSocket(const char*& initError);
-	void InstallAddressList();
+	void InitWebSocket();
+	void ShutdownWebSocket();
+
+	void ToggleInitWebSocket();
+
+	void RebootWebSocket();
+
+	void CreateLogger();
+	void DestroyLogger();
 
 	void RunFrame();
-	void LogEvent(const char* const dataBuf, const int32_t dataSize);
+	void LogEvent(const google::protobuf::Message* const toTransmit, const google::protobuf::Message* toPrint);
 
 	bool IsEnabled() const;
+	bool IsValidToRun() const;
+
 	inline bool WebSocketInitialized() const { return webSocketSystem.IsInitialized(); }
+	inline bool FileLoggerInitialized() const { return matchLogger != nullptr; }
 
 private:
 	CWebSocket webSocketSystem;
+
+	std::shared_ptr<spdlog::logger> matchLogger;
+	int matchLogCount;
+	bool initialLog;
+	bool initialized;
 };
 
 LiveAPI* LiveAPISystem();