550 lines
17 KiB
C++
550 lines
17 KiB
C++
// Fill out your copyright notice in the Description page of Project Settings.
|
|
|
|
|
|
#include "DTFluxRemoteSubsystem.h"
|
|
#include "DTFluxRemoteSubsystem.h"
|
|
|
|
#include "DTFluxGeneralSettings.h"
|
|
#include "DTFluxRemoteModule.h"
|
|
#include "DTFluxRemoteModule.h"
|
|
#include "HttpServerModule.h"
|
|
#include "IHttpRouter.h"
|
|
#include "Rundown/AvaRundown.h"
|
|
#include "Json.h"
|
|
#include "JsonObjectConverter.h"
|
|
#include "Engine/Engine.h"
|
|
#include "Misc/DateTime.h"
|
|
|
|
|
|
void UDTFluxRemoteSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
|
{
|
|
Super::Initialize(Collection);
|
|
|
|
UE_LOG(logDTFluxRemote, Log, TEXT("DTFlux API Subsystem Initialized"));
|
|
|
|
#if WITH_EDITOR
|
|
// S'abonner aux changements de settings
|
|
if (UDTFluxGeneralSettings* Settings = GetMutableDefault<UDTFluxGeneralSettings>())
|
|
{
|
|
SettingsRundownChangedHandle = Settings->OnRemoteRundownChanged.AddUObject(
|
|
this, &UDTFluxRemoteSubsystem::OnSettingsRundownChanged
|
|
);
|
|
}
|
|
#endif
|
|
LoadRundownFromSettings();
|
|
// Auto-start server (optionnel)
|
|
StartHTTPServer(63350);
|
|
}
|
|
|
|
void UDTFluxRemoteSubsystem::Deinitialize()
|
|
{
|
|
StopHTTPServer();
|
|
|
|
|
|
#if WITH_EDITOR
|
|
// Se désabonner du delegate
|
|
if (UDTFluxGeneralSettings* Settings = GetMutableDefault<UDTFluxGeneralSettings>())
|
|
{
|
|
if (SettingsRundownChangedHandle.IsValid())
|
|
{
|
|
Settings->OnRemoteRundownChanged.Remove(SettingsRundownChangedHandle);
|
|
SettingsRundownChangedHandle.Reset();
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// Décharger proprement le rundown
|
|
UnloadCurrentRundown();
|
|
UE_LOG(logDTFluxRemote, Log, TEXT("DTFlux API Subsystem Deinitialized"));
|
|
|
|
Super::Deinitialize();
|
|
}
|
|
|
|
bool UDTFluxRemoteSubsystem::StartHTTPServer(int32 Port)
|
|
{
|
|
if (bServerRunning)
|
|
{
|
|
UE_LOG(logDTFluxRemote, Warning, TEXT("HTTP Server already running on port %d"), ServerPort);
|
|
return true;
|
|
}
|
|
|
|
ServerPort = Port;
|
|
|
|
// Get HTTP Server Module
|
|
FHttpServerModule& HttpServerModule = FHttpServerModule::Get();
|
|
|
|
// Create router
|
|
HttpRouter = HttpServerModule.GetHttpRouter(ServerPort);
|
|
|
|
if (!HttpRouter.IsValid())
|
|
{
|
|
UE_LOG(logDTFluxRemote, Error, TEXT("Failed to create HTTP router for port %d"), ServerPort);
|
|
return false;
|
|
}
|
|
|
|
// Setup routes
|
|
SetupRoutes();
|
|
|
|
// Start listening
|
|
HttpServerModule.StartAllListeners();
|
|
|
|
bServerRunning = true;
|
|
|
|
UE_LOG(logDTFluxRemote, Log, TEXT("DTFlux HTTP API Server started on port %d"), ServerPort);
|
|
UE_LOG(logDTFluxRemote, Log, TEXT("Base URL: http://localhost:%d/dtflux/api/v1"), ServerPort);
|
|
UE_LOG(logDTFluxRemote, Log, TEXT("Available routes:"));
|
|
UE_LOG(logDTFluxRemote, Log, TEXT(" PUT /dtflux/api/v1/title"));
|
|
UE_LOG(logDTFluxRemote, Log, TEXT(" PUT /dtflux/api/v1/title-bib"));
|
|
UE_LOG(logDTFluxRemote, Log, TEXT(" PUT /dtflux/api/v1/commands"));
|
|
|
|
return true;
|
|
}
|
|
|
|
void UDTFluxRemoteSubsystem::StopHTTPServer()
|
|
{
|
|
if (!bServerRunning)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Remove route handlers
|
|
if (HttpRouter.IsValid())
|
|
{
|
|
HttpRouter->UnbindRoute(TitleRouteHandle);
|
|
HttpRouter->UnbindRoute(TitleBibRouteHandle);
|
|
HttpRouter->UnbindRoute(CommandsRouteHandle);
|
|
}
|
|
|
|
// Stop server
|
|
FHttpServerModule& HttpServerModule = FHttpServerModule::Get();
|
|
HttpServerModule.StopAllListeners();
|
|
|
|
HttpRouter.Reset();
|
|
bServerRunning = false;
|
|
|
|
UE_LOG(logDTFluxRemote, Log, TEXT("DTFlux HTTP API Server stopped"));
|
|
}
|
|
|
|
bool UDTFluxRemoteSubsystem::IsHTTPServerRunning() const
|
|
{
|
|
return bServerRunning;
|
|
}
|
|
|
|
void UDTFluxRemoteSubsystem::ResetPendingTitleData()
|
|
{
|
|
bHasPendingTitleRequest = false;
|
|
}
|
|
|
|
void UDTFluxRemoteSubsystem::ResetPendingBibData()
|
|
{
|
|
bHasPendingTitleBibRequest = false;
|
|
}
|
|
|
|
void UDTFluxRemoteSubsystem::SetupRoutes()
|
|
{
|
|
if (!HttpRouter.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Route: POST /dtflux/api/v1/title
|
|
TitleRouteHandle = HttpRouter->BindRoute(
|
|
FHttpPath(TEXT("/dtflux/api/v1/title")),
|
|
EHttpServerRequestVerbs::VERB_PUT,
|
|
FHttpRequestHandler::CreateUObject(this, &UDTFluxRemoteSubsystem::HandleTitleRequest)
|
|
);
|
|
|
|
// Route: POST /dtflux/api/v1/title-bib
|
|
TitleBibRouteHandle = HttpRouter->BindRoute(
|
|
FHttpPath(TEXT("/dtflux/api/v1/title-bib")),
|
|
EHttpServerRequestVerbs::VERB_PUT,
|
|
FHttpRequestHandler::CreateUObject(this, &UDTFluxRemoteSubsystem::HandleTitleBibRequest)
|
|
);
|
|
|
|
// Route: POST /dtflux/api/v1/commands
|
|
CommandsRouteHandle = HttpRouter->BindRoute(
|
|
FHttpPath(TEXT("/dtflux/api/v1/commands")),
|
|
EHttpServerRequestVerbs::VERB_PUT,
|
|
FHttpRequestHandler::CreateUObject(this, &UDTFluxRemoteSubsystem::HandleCommandsRequest)
|
|
);
|
|
|
|
UE_LOG(logDTFluxRemote, Log, TEXT("HTTP routes configured successfully"));
|
|
}
|
|
|
|
bool UDTFluxRemoteSubsystem::HandleTitleRequest(const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete)
|
|
{
|
|
UE_LOG(logDTFluxRemote, Log, TEXT("Received Title request"));
|
|
|
|
// Parse JSON
|
|
TSharedPtr<FJsonObject> JsonObject = ParseJsonFromRequest(Request);
|
|
if (!JsonObject.IsValid())
|
|
{
|
|
OnComplete(FHttpServerResponse::Create(CreateErrorResponse(TEXT("Invalid JSON")), TEXT("application/json")));
|
|
return true;
|
|
}
|
|
|
|
// Parse title data
|
|
FDTFluxRemoteTitleData TitleData;
|
|
if (!ParseTitleData(JsonObject, TitleData))
|
|
{
|
|
OnComplete(FHttpServerResponse::Create(CreateErrorResponse(TEXT("Invalid title data format")), TEXT("application/json")));
|
|
return true;
|
|
}
|
|
|
|
// Broadcast event (execute on Game Thread)
|
|
AsyncTask(ENamedThreads::GameThread, [this, TitleData]()
|
|
{
|
|
OnTitleReceived.Broadcast(TitleData);
|
|
if (RemotedRundown && RemotedRundown->IsValidLowLevel())
|
|
{
|
|
PendingTitleData = TitleData;
|
|
bHasPendingTitleRequest = true;
|
|
UE_LOG(logDTFluxRemote, Log, TEXT("Playing page %i"), TitleData.RundownPageId);
|
|
RemotedRundown->PlayPage(TitleData.RundownPageId, EAvaRundownPagePlayType::PlayFromStart);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(logDTFluxRemote, Warning, TEXT("No rundown loaded"));
|
|
}
|
|
});
|
|
|
|
OnComplete(FHttpServerResponse::Create(CreateSuccessResponse(TEXT("Title data received")), TEXT("application/json")));
|
|
return true;
|
|
}
|
|
|
|
bool UDTFluxRemoteSubsystem::HandleTitleBibRequest(const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete)
|
|
{
|
|
UE_LOG(logDTFluxRemote, Log, TEXT("Received Title-Bib request"));
|
|
|
|
TSharedPtr<FJsonObject> JsonObject = ParseJsonFromRequest(Request);
|
|
if (!JsonObject.IsValid())
|
|
{
|
|
OnComplete(FHttpServerResponse::Create(CreateErrorResponse(TEXT("Invalid JSON")), TEXT("application/json")));
|
|
return true;
|
|
}
|
|
|
|
FDTFluxRemoteBibData BibData;
|
|
if (!ParseTitleBibData(JsonObject, BibData))
|
|
{
|
|
OnComplete(FHttpServerResponse::Create(CreateErrorResponse(TEXT("Invalid title-bib data format")), TEXT("application/json")));
|
|
return true;
|
|
}
|
|
|
|
AsyncTask(ENamedThreads::GameThread, [this, BibData]()
|
|
{
|
|
OnTitleBibReceived.Broadcast(BibData);
|
|
});
|
|
|
|
OnComplete(FHttpServerResponse::Create(CreateSuccessResponse(TEXT("Title-bib data received")), TEXT("application/json")));
|
|
return true;
|
|
}
|
|
|
|
bool UDTFluxRemoteSubsystem::HandleCommandsRequest(const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete)
|
|
{
|
|
UE_LOG(logDTFluxRemote, Log, TEXT("Received Commands request"));
|
|
|
|
TSharedPtr<FJsonObject> JsonObject = ParseJsonFromRequest(Request);
|
|
if (!JsonObject.IsValid())
|
|
{
|
|
OnComplete(FHttpServerResponse::Create(CreateErrorResponse(TEXT("Invalid JSON")), TEXT("application/json")));
|
|
return true;
|
|
}
|
|
|
|
FDTFluxRemoteCommandData CommandData;
|
|
if (!ParseCommandData(JsonObject, CommandData))
|
|
{
|
|
OnComplete(FHttpServerResponse::Create(CreateErrorResponse(TEXT("Invalid command data format")), TEXT("application/json")));
|
|
return true;
|
|
}
|
|
|
|
AsyncTask(ENamedThreads::GameThread, [this, CommandData]()
|
|
{
|
|
OnCommandReceived.Broadcast(CommandData);
|
|
if (RemotedRundown && RemotedRundown->IsValidLowLevel())
|
|
{
|
|
RemotedRundown->StopPage(CommandData.RundownPageId, EAvaRundownPageStopOptions::None, false);
|
|
UE_LOG(logDTFluxRemote, Log, TEXT("Stoping page %i"), CommandData.RundownPageId);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(logDTFluxRemote, Warning, TEXT("No rundown loaded"));
|
|
}
|
|
});
|
|
|
|
OnComplete(FHttpServerResponse::Create(CreateErrorResponse(TEXT("OK")), TEXT("application/json")));
|
|
return true;
|
|
}
|
|
|
|
TSharedPtr<FJsonObject> UDTFluxRemoteSubsystem::ParseJsonFromRequest(const FHttpServerRequest& Request)
|
|
{
|
|
// Get request body
|
|
TArray<uint8> Body = Request.Body;
|
|
FString JsonString;
|
|
if (Body.Num() > 0)
|
|
{
|
|
// Ajouter un null terminator si nécessaire
|
|
if (Body.Last() != 0)
|
|
{
|
|
Body.Add(0);
|
|
}
|
|
|
|
JsonString = FString(UTF8_TO_TCHAR(reinterpret_cast<const char*>(Body.GetData())));
|
|
}
|
|
|
|
UE_LOG(logDTFluxRemote, Verbose, TEXT("Received JSON: %s"), *JsonString);
|
|
|
|
// Parse JSON
|
|
TSharedPtr<FJsonObject> JsonObject;
|
|
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonString);
|
|
|
|
if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid())
|
|
{
|
|
UE_LOG(logDTFluxRemote, Error, TEXT("Failed to parse JSON: %s"), *JsonString);
|
|
return nullptr;
|
|
}
|
|
|
|
return JsonObject;
|
|
}
|
|
|
|
FString UDTFluxRemoteSubsystem::CreateSuccessResponse(const FString& Message)
|
|
{
|
|
TSharedPtr<FJsonObject> ResponseObject = MakeShareable(new FJsonObject);
|
|
ResponseObject->SetBoolField(TEXT("success"), true);
|
|
ResponseObject->SetStringField(TEXT("message"), Message);
|
|
ResponseObject->SetStringField(TEXT("timestamp"), FDateTime::Now().ToIso8601());
|
|
|
|
FString OutputString;
|
|
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputString);
|
|
FJsonSerializer::Serialize(ResponseObject.ToSharedRef(), Writer);
|
|
|
|
return OutputString;
|
|
}
|
|
|
|
FString UDTFluxRemoteSubsystem::CreateErrorResponse(const FString& Error, int32 Code)
|
|
{
|
|
TSharedPtr<FJsonObject> ResponseObject = MakeShareable(new FJsonObject);
|
|
ResponseObject->SetBoolField(TEXT("success"), false);
|
|
ResponseObject->SetStringField(TEXT("error"), Error);
|
|
ResponseObject->SetNumberField(TEXT("code"), Code);
|
|
ResponseObject->SetStringField(TEXT("timestamp"), FDateTime::Now().ToIso8601());
|
|
|
|
FString OutputString;
|
|
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputString);
|
|
FJsonSerializer::Serialize(ResponseObject.ToSharedRef(), Writer);
|
|
|
|
return OutputString;
|
|
}
|
|
|
|
bool UDTFluxRemoteSubsystem::ParseTitleData(const TSharedPtr<FJsonObject>& JsonObject, FDTFluxRemoteTitleData& OutData)
|
|
{
|
|
if (!JsonObject.IsValid())
|
|
{
|
|
UE_LOG(logDTFluxRemote, Error, TEXT("Invalid JSON object for %s"), TEXT("FDTFluxRemoteTitleData"));
|
|
return false;
|
|
}
|
|
|
|
if (FJsonObjectConverter::JsonObjectToUStruct<FDTFluxRemoteTitleData>(JsonObject.ToSharedRef(), &OutData))
|
|
{
|
|
UE_LOG(logDTFluxRemote, Log, TEXT("Successfully parsed %s"), TEXT("FDTFluxRemoteTitleData"));
|
|
return true;
|
|
}
|
|
|
|
UE_LOG(logDTFluxRemote, Error, TEXT("Failed to convert JSON to %s struct"),TEXT("FDTFluxRemoteTitleData"));
|
|
return false;
|
|
}
|
|
|
|
bool UDTFluxRemoteSubsystem::ParseTitleBibData(const TSharedPtr<FJsonObject>& JsonObject, FDTFluxRemoteBibData& OutData)
|
|
{
|
|
if (!JsonObject.IsValid())
|
|
{
|
|
UE_LOG(logDTFluxRemote, Error, TEXT("Invalid JSON object for %s"), TEXT("FDTFluxRemoteBibData"));
|
|
return false;
|
|
}
|
|
|
|
if (FJsonObjectConverter::JsonObjectToUStruct<FDTFluxRemoteBibData>(JsonObject.ToSharedRef(), &OutData))
|
|
{
|
|
UE_LOG(logDTFluxRemote, Log, TEXT("Successfully parsed %s"), TEXT("FDTFluxRemoteBibData"));
|
|
return true;
|
|
}
|
|
|
|
UE_LOG(logDTFluxRemote, Error, TEXT("Failed to convert JSON to %s struct"),TEXT("FDTFluxRemoteBibData"));
|
|
return false;
|
|
}
|
|
|
|
bool UDTFluxRemoteSubsystem::ParseCommandData(const TSharedPtr<FJsonObject>& JsonObject, FDTFluxRemoteCommandData& OutData)
|
|
{
|
|
if (!JsonObject.IsValid())
|
|
{
|
|
UE_LOG(logDTFluxRemote, Error, TEXT("Invalid JSON object for %s"), TEXT("FDTFluxRemoteCommandData"));
|
|
return false;
|
|
}
|
|
|
|
if (FJsonObjectConverter::JsonObjectToUStruct<FDTFluxRemoteCommandData>(JsonObject.ToSharedRef(), &OutData))
|
|
{
|
|
UE_LOG(logDTFluxRemote, Log, TEXT("Successfully parsed %s"), TEXT("FDTFluxRemoteCommandData"));
|
|
return true;
|
|
}
|
|
|
|
UE_LOG(logDTFluxRemote, Error, TEXT("Failed to convert JSON to %s struct"),TEXT("FDTFluxRemoteCommandData"));
|
|
return false;
|
|
}
|
|
|
|
void UDTFluxRemoteSubsystem::UnloadCurrentRundown()
|
|
{
|
|
if (RemotedRundown)
|
|
{
|
|
UE_LOG(logDTFluxRemote, Log, TEXT("Unloading current rundown"));
|
|
// Ici vous pouvez ajouter une logique de nettoyage si nécessaire
|
|
// Par exemple : RemotedRundown->StopAllPages();
|
|
RemotedRundown = nullptr;
|
|
}
|
|
}
|
|
|
|
void UDTFluxRemoteSubsystem::LoadRundownFromSettings()
|
|
{
|
|
const UDTFluxGeneralSettings* Settings = GetDefault<UDTFluxGeneralSettings>();
|
|
if (!Settings)
|
|
{
|
|
UE_LOG(logDTFluxRemote, Warning, TEXT("Cannot access DTFlux settings"));
|
|
return;
|
|
}
|
|
|
|
TSoftObjectPtr<UAvaRundown> RundownAsset = Settings->RemoteTargetRundown;
|
|
|
|
if (RundownAsset.IsNull())
|
|
{
|
|
UE_LOG(logDTFluxRemote, Log, TEXT("No rundown specified in settings"));
|
|
UnloadCurrentRundown();
|
|
return;
|
|
}
|
|
|
|
if (RemotedRundown && RemotedRundown == RundownAsset.LoadSynchronous())
|
|
{
|
|
UE_LOG(logDTFluxRemote, Log, TEXT("Rundown already loaded: %s"), *RundownAsset.ToString());
|
|
return;
|
|
}
|
|
|
|
// Décharger l'ancien rundown d'abord
|
|
UnloadCurrentRundown();
|
|
RundownAsset = RundownAsset.LoadSynchronous();
|
|
// Charger le nouveau rundown
|
|
if ( RundownAsset.IsValid())
|
|
{
|
|
UE_LOG(logDTFluxRemote, Log, TEXT("Successfully loaded rundown from settings: %s"), *RundownAsset.ToString());
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(logDTFluxRemote, Error, TEXT("Failed to load rundown from settings: %s"), *RundownAsset.ToString());
|
|
}
|
|
LoadRundown(RundownAsset);
|
|
}
|
|
|
|
bool UDTFluxRemoteSubsystem::LoadRundown(const TSoftObjectPtr<UAvaRundown>& RundownAsset)
|
|
{
|
|
if (RundownAsset.IsNull())
|
|
{
|
|
UE_LOG(logDTFluxRemote, Warning, TEXT("Cannot load rundown: asset is null"));
|
|
UnloadCurrentRundown();
|
|
return false;
|
|
}
|
|
|
|
// Charger le rundown de manière synchrone
|
|
UAvaRundown* LoadedRundown = RundownAsset.LoadSynchronous();
|
|
|
|
// Vérifier si le rundown est déjà chargé
|
|
if (RemotedRundown && RemotedRundown == LoadedRundown)
|
|
{
|
|
UE_LOG(logDTFluxRemote, Log, TEXT("Rundown already loaded: %s"), *RundownAsset.ToString());
|
|
return true;
|
|
}
|
|
|
|
// Décharger l'ancien rundown d'abord
|
|
UnloadCurrentRundown();
|
|
|
|
// Assigner le nouveau rundown
|
|
RemotedRundown = LoadedRundown;
|
|
|
|
// Vérifier que le chargement a réussi
|
|
if (RemotedRundown && RemotedRundown->IsValidLowLevel())
|
|
{
|
|
RemotedRundown->InitializePlaybackContext();
|
|
UE_LOG(logDTFluxRemote, Log, TEXT("Successfully loaded rundown: %s"), *RundownAsset.ToString());
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(logDTFluxRemote, Error, TEXT("Failed to load rundown: %s"), *RundownAsset.ToString());
|
|
RemotedRundown = nullptr;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
void UDTFluxRemoteSubsystem::OnSettingsRundownChanged(const TSoftObjectPtr<UAvaRundown>& NewRundown)
|
|
{
|
|
}
|
|
#endif
|
|
|
|
|
|
// Manual processing functions for testing
|
|
bool UDTFluxRemoteSubsystem::ProcessTitleData(const FString& JsonString)
|
|
{
|
|
TSharedPtr<FJsonObject> JsonObject;
|
|
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonString);
|
|
|
|
if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
FDTFluxRemoteTitleData TitleData;
|
|
if (ParseTitleData(JsonObject, TitleData))
|
|
{
|
|
OnTitleReceived.Broadcast(TitleData);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool UDTFluxRemoteSubsystem::ProcessTitleBibData(const FString& JsonString)
|
|
{
|
|
TSharedPtr<FJsonObject> JsonObject;
|
|
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonString);
|
|
|
|
if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FDTFluxRemoteBibData TitleBibData;
|
|
if (ParseTitleBibData(JsonObject, TitleBibData))
|
|
{
|
|
OnTitleBibReceived.Broadcast(TitleBibData);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool UDTFluxRemoteSubsystem::ProcessCommandData(const FString& JsonString)
|
|
{
|
|
TSharedPtr<FJsonObject> JsonObject;
|
|
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonString);
|
|
|
|
if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FDTFluxRemoteCommandData CommandData;
|
|
if (ParseCommandData(JsonObject, CommandData))
|
|
{
|
|
OnCommandReceived.Broadcast(CommandData);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
|