diff --git a/LICENSE b/LICENSE index d22fb65f..b83e3c77 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,11 @@ The MIT License (MIT) +Socket.IO Client UE4 Copyright (c) 2016 Jan Kaniewski (Getnamo) +VARest +Copyright (c) 2014 Vladimir Alyamkin + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights diff --git a/README.md b/README.md index b1f6409f..7b7e4af3 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ socket.io client plugin for UE4 [Socket.io](http://socket.io/) is a performant real-time bi-directional communication library. There are two parts, the server written in node.js and the client typically javascript for the web. There are alternative client implementations and this repo uses the [C++11 client library](https://github.com/socketio/socket.io-client-cpp) ported to UE4. -Uses [Socket.io prebuild libraries for VS2015](https://github.com/getnamo/socketio-client-prebuild) +Uses [Socket.io prebuild libraries for VS2015](https://github.com/getnamo/socketio-client-prebuild) and SIOJson forked from ufna's [VaRest](https://github.com/ufna/VaRest) [Unreal Forum Thread](https://forums.unrealengine.com/showthread.php?110680-Plugin-Socket-io-Client) @@ -26,23 +26,100 @@ Add the SocketIO Client Component to your blueprint actor of choice ![IMG](http://i.imgur.com/lSkfHQ2.png) -Specify your address and port, defaults to localhost (127.0.0.1) at port 3000 +By default the component will auto connect *on begin play* to your default address and port [http://localhost:3000](http://localhost:3000). You can change this default address to connect to your service instead. -![IMG](http://i.imgur.com/rjm2pKw.png) +![IMG](http://i.imgur.com/dWxCmvQ.png) + +If you want to connect at your own time, you change the default variable *Should Auto Connect* to false and then call *Connect* with your address Call *Bind Event* for each event you wish the client to subscribe, e.g. 'chat message' If you expect to receive events, select your component and in the Details pane press the + to add an 'On' event to your event graph -![IMG](http://i.imgur.com/mZVTJWE.png) - Handle this event for your event types, e.g. printing 'chat message' event strings. -![IMG](http://i.imgur.com/0BCFOoS.png) +![IMG](http://i.imgur.com/vVlNBlx.png) + If you want to send information to the server, emit events on the SocketIO Client Component, e.g. pressing M to emit a 'chat message' string -![IMG](http://i.imgur.com/EOadatA.png) +![IMG](http://i.imgur.com/nihMSPz.png) + +## Blueprint - Advanced + +### Simple Json + +You can formulate any *SIOJsonValue* directly in blueprint. Apart from native basic types which are supported directly via conversion and e.g. *Construct Json String Value*, you can construct *SIOJsonObject*s and fill their fields. + +![IMG](http://i.imgur.com/PnxD6Ui.png) + +Start with *Construct Json Object* then set any desired fields. In this instance we wanted to make the JSON *{"myString":"Hello"}* so we used *Set String Field* and then auto-converted the object into a message. + +### Complex Json + +By combining arrays and objects you can form almost any data type, nest away! + +![IMG](http://i.imgur.com/lj07Jsw.png) + +### Structs + +The plugin supports making *SIOJsonValue*s from any unreal structs you make, including ones defined purely in blueprints! + +An easy example of a familiar struct is the *Vector* type + +![IMG](http://i.imgur.com/mPHOw0C.png) + +But you can make a custom type and emit it or nest it inside other *SIOJsonValue*s which should make it easy to organize your data however you want it. + +![IMG](http://i.imgur.com/czi0AIF.png) + +### Binary + +Socket.IO spec supports raw binary data types and these should be capable of being mixed in with other JSON types as usual. This plugin supports binaries as a first class citizen in blueprints and any arrays of bytes can be embedded and decoded in the chain as expected. + +![IMG](http://i.imgur.com/PqxEJqI.png) + +### Decoding Responses + +There are many ways to decode your *SIOJsonValue* message, it all depends on your data setup. You can even decode your *JsonObject*s directly into structs, if the JSON structure has matching variable names. + +![IMG](http://i.imgur.com/urAh2TH.png) + +### Conversion + +Most primitive types have auto-conversion nodes to simplify your workflow. E.g. if you wanted to emit a float literal you can get a reference to your float and simply drag to the *message* parameter to auto-convert into a *SIOJsonValue*. + +![IMG](http://i.imgur.com/4T79TUV.gif) + +Supported auto-conversion + +
  • Float
  • +
  • Int
  • +
  • Bool
  • +
  • String
  • +
  • SIOJsonObject
  • + +### Emit with Callback + +You can have a callback when, for example, you need an acknowledgement or if you're fetching data from the server. You can respond to this callback straight in your blueprint. + +![IMG](http://i.imgur.com/Ed01Jq0.png) + +Instead of using *Emit* you use *Emit With Callback* and by default the target is the owning actor blueprint so you can leave that parameter blank and simply type your function name e.g. *OnEcho* function. + +![IMG](http://i.imgur.com/hXMXDd2.png) + +If the function is missing or named incorrectly, your output log will warn you. + +![IMG](http://i.imgur.com/PQinDYy.gif) + +Your function expects a *SIOJsonValue* reference signature. By default this contains your first response value from you callback parameter. If your callback uses more than one parameter, make a second *SIOJsonValue* Input parameter which contains an array of all the responses. + +### Binding Events to Functions + +Instead of using the event graph and comparing strings, you can bind an event directly to a function. The format to make the function is the same as callbacks. + +![IMG](http://i.imgur.com/7fA1qca.png) ## How to use - C++ @@ -56,20 +133,22 @@ To use the C++ code from the plugin add it as a dependency module in your projec and *CreateDefaultSubobject* in your constructor -```SocketIOClientComponent = CreateDefaultSubobject(TEXT("SocketIOClientComponent"));``` +```c++ +SIOClientComponent = CreateDefaultSubobject(TEXT("SocketIOClientComponent")); +``` or reference it from another component by getting it on begin play e.g. -``` -SIOComponent = Cast(this->GetOwner()->GetComponentByClass(USocketIOClientComponent::StaticClass())); -if (!SIOComponent) +```c++ +SIOClientComponent = Cast(this->GetOwner()->GetComponentByClass(USocketIOClientComponent::StaticClass())); +if (!SIOClientComponent) { UE_LOG(LogTemp, Warning, TEXT("No sister socket IO component found")); return; } else { - UE_LOG(LogTemp, Log, TEXT("Found SIOComponent: %s"), *SIOComponent->GetDesc()); + UE_LOG(LogTemp, Log, TEXT("Found SIOClientComponent: %s"), *SIOComponent->GetDesc()); } ``` @@ -79,58 +158,189 @@ To connect simply change your address, the component will auto-connect on compon ``` -USocketIOClientComponent* SIOComponent; //get a reference or add as subobject in your actor +USocketIOClientComponent* SIOClientComponent; //get a reference or add as subobject in your actor //the component will autoconnect, but you may wish to change the url before it does that via -SIOComponent->AddressAndPort = FString("http://127.0.0.1:3000"); //change your address +SIOClientComponent->AddressAndPort = FString("http://127.0.0.1:3000"); //change your address ``` You can also connect at your own time by disabling auto-connect and connecting either to the default address or a custom one ``` //you can also disable auto connect and connect it at your own time via -SIOComponent->ShouldAutoConnect = false; -SIOComponent->Connect(); +SIOClientComponent->ShouldAutoConnect = false; +SIOClientComponent->Connect(); //You can also easily disconnect at some point, reconnect to another address -SIOComponent->Disconnect(); -SIOComponent->Connect(FString("http://127.0.0.1:3000")); +SIOClientComponent->Disconnect(); +SIOClientComponent->Connect(FString("http://127.0.0.1:3000")); ``` ### Emitting Events +In C++ you can use *EmitNative*, *EmitRaw*, or *EmitRawBinary*. *EmitNative* is fully overloaded and expects all kinds of native UE4 data types and is the recommended method. + ####String -Emit a string via +Emit an FString. Note that *FString(TEXT("yourString"))* is recommended if you have performance concerns due to internal conversion from ```char*``` + +```c++ +SIOClientComponent->EmitNative(FString("nativeTest"), FString("hi")); +``` + +####Number -```SIOComponent->Emit(FString("myevent"), FString(TEXT("some data or stringified json"));``` +Emit a double + +```c++ +SIOClientComponent->EmitNative(FString("nativeTest"), -3.5f); +``` + +####Boolean + +Emit a raw boolean + +```c++ +SIOClientComponent->EmitNative(FString("nativeTest"), true); +``` ####Binary or raw data -Emit raw data via +Emit raw data via a TArray +```c++ +TArray Buffer; //null terminated 'Hi!' +Buffer.Add(0x48); +Buffer.Add(0x69); +Buffer.Add(0x21); +Buffer.Add(0x00); + +SIOClientComponent->EmitNative(FString("nativeTest"), Buffer); ``` -TArray Buffer; -//fill buffer with your data +or -SIOComponent->EmitBuffer(FString("myBinarySendEvent"), Buffer.GetData(), Buffer.Num()); +```c++ +SIOComponent->EmitRawBinary(FString("myBinarySendEvent"), Buffer.GetData(), Buffer.Num()); ``` -####Complex message using sio::message +####FJsonObject - Simple -see [sio::message](https://github.com/socketio/socket.io-client-cpp/blob/master/src/sio_message.h) for how to form a raw message. Generally it supports a lot of std:: variants e.g. std::string or more complex messages e.g. [socket.io c++ emit readme](https://github.com/socketio/socket.io-client-cpp#emit-an-event). Note that there are static helper functions attached to the component class to convert from std::string to FString and the reverse. +Option 1 - Shorthand + +```c++ +//Basic one field object e.g. {"myKey":"myValue"} +auto JsonObject = USIOJConvert::MakeJsonObject(); +JsonObject->SetStringField(FString("myKey"), FString("myValue")); +SIOClientComponent->EmitNative(FString("nativeTest"), JsonObject); ``` -static std::string StdString(FString UEString); -static FString FStringFromStd(std::string StdString); +Option 2 - Standard + +```c++ +TSharedPtr JsonObject = MakeShareable(new FJsonObject); +``` + +####FJsonObject - Complex Example + +A nested example using various methods + +```c++ +//All types, nested +TSharedPtr JsonObject = MakeShareable(new FJsonObject); //make object option2 +JsonObject->SetBoolField(FString("myBool"), false); +JsonObject->SetStringField(FString("myString"), FString("Socket.io is easy")); +JsonObject->SetNumberField(FString("myNumber"), 9001); + +JsonObject->SetField(FString("myBinary1"), USIOJConvert::ToJsonValue(Buffer)); //binary option1 - shorthand +JsonObject->SetField(FString("myBinary2"), MakeShareable(new FJsonValueBinary(Buffer))); //binary option2 + +JsonObject->SetArrayField(FString("myArray"), ArrayValue); +JsonObject->SetObjectField(FString("myNestedObject"), SmallObject); + +SIOClientComponent->EmitNative(FString("nativeTest"), JsonObject); ``` - -e.g. emitting {type:"image"} object +####Callback Example +Below is an example of emitting a simple object with the server using the passed in callback to return a response or acknowledgement. + +```c++ +//Make an object {"myKey":"myValue"} +TSharedPtr JsonObject = MakeShareable(new FJsonObject); +JsonObject->SetStringField(FString("myKey"), FString("myValue")); + +//Show what we emitted +UE_LOG(LogTemp, Log, TEXT("1) Made a simple object and emitted: %s"), *USIOJConvert::ToJsonString(JsonObject)); + +//Emit event "callbackTest" expecting an echo callback with the object we sent +SIOClientComponent->EmitNative(FString("callbackTest"), JsonObject, [&](auto Response) +{ + //Response is an array of JsonValues, in our case we expect an object response, grab first element as an object. + auto Message = Response[0]->AsObject(); + + //Show what we received + UE_LOG(LogTemp, Log, TEXT("2) Received a response: %s"), *USIOJConvert::ToJsonString(Message)); +}); ``` + +####UStruct + +Plugin supports automatic conversion to/from UStructs, below is an example of a struct roundtrip, being in Json format on the server side. + +```c++ +USTRUCT() +struct FTestCppStruct +{ + GENERATED_BODY() + + UPROPERTY() + int32 Index; + + UPROPERTY() + float SomeNumber; + + UPROPERTY() + FString Name; +}; +``` + +```c++ +//Set your struct variables +FTestCppStruct TestStruct; +TestStruct.Name = FString("George"); +TestStruct.Index = 5; +TestStruct.SomeNumber = 5.123f; + +SIOClientComponent->EmitNative(FString("callbackTest"), FTestCppStruct::StaticStruct(), &TestStruct, [&](auto Response) +{ + auto Message = Response[0]->AsObject(); + + //Show what we received + UE_LOG(LogTemp, Log, TEXT("Received a response: %s"), *USIOJConvert::ToJsonString(Message)); + + //Set our second struct to the new values + USIOJConvert::JsonObjectToUStruct(Message, FTestCppStruct::StaticStruct(), &MemberStruct); + + //Show that struct + UE_LOG(LogTemp, Log, TEXT("Our received member name is now: %s"), *MemberStruct.Name); +}); +``` + +###Alternative Raw C++ Complex message using sio::message + +see [sio::message](https://github.com/socketio/socket.io-client-cpp/blob/master/src/sio_message.h) for how to form a raw message. Generally it supports a lot of std:: variants e.g. std::string or more complex messages e.g. [socket.io c++ emit readme](https://github.com/socketio/socket.io-client-cpp#emit-an-event). Note that there are static helper functions attached to the component class to convert from std::string to FString and the reverse. + +```c++ +static std::string USIOMessageConvert::StdString(FString UEString); + +static FString USIOMessageConvert::FStringFromStd(std::string StdString); +``` + +e.g. emitting *{type:"image"}* object + +```c++ //create object message auto message = sio::object_message::create(); @@ -143,7 +353,7 @@ SIOComponent->EmitRaw(ShareResourceEventName, message); with a callback -``` +```c++ SIOComponent->EmitRawWithCallback(FString("myRawMessageEventWithAck"), string_message::create(username), [&](message::list const& msg) { //got data, handle it here }); @@ -154,19 +364,10 @@ SIOComponent->EmitRawWithCallback(FString("myRawMessageEventWithAck"), string_me To receive events you can bind lambdas which makes things awesomely easy e.g. -#### String - -``` -SIOComponent->BindStringMessageLambdaToEvent([&](const FString& Name, const FString& Data) - { - //do something with your string data - }, FString(TEXT("myStringReceiveEvent"))); -``` - #### Binary -``` -SIOComponent->BindBinaryMessageLambdaToEvent([&](const FString& Name, const TArray& Buffer) +```c++ +SIOComponent->OnBinaryEvent([&](const FString& Name, const TArray& Buffer) { //Do something with your buffer }, FString(TEXT("myBinaryReceiveEvent"))); @@ -178,8 +379,8 @@ Currently the only way to handle json messages as the plugin doesn't auto-conver e.g. expecting a result {type:"some type"} -``` -SIOComponent->BindRawMessageLambdaToEvent([&](const FString& Name, const sio::message::ptr& Message) +```c++ +SIOComponent->OnRawEvent([&](const FString& Name, const sio::message::ptr& Message) { //if you expected an object e.g. {} if (Message->get_flag() != sio::message::flag_object) diff --git a/SocketIOClient.uplugin b/SocketIOClient.uplugin index cb74c138..ee99ca5f 100644 --- a/SocketIOClient.uplugin +++ b/SocketIOClient.uplugin @@ -1,7 +1,7 @@ { "FileVersion": 3, "Version": 1, - "VersionName": "0.2.5", + "VersionName": "0.4.0", "FriendlyName": "SocketIOClient", "Description": "Socket IO C++ Client ported to UE4", "Category": "Networking", @@ -17,6 +17,15 @@ "Name": "SocketIOClient", "Type": "Runtime", "LoadingPhase": "Default" + }, + { + "Name" : "SIOJson", + "Type" : "Runtime", + "LoadingPhase": "PreDefault" + }, + { + "Name": "SIOJEditorPlugin", + "Type": "Developer" } ] } \ No newline at end of file diff --git a/Source/SIOJEditorPlugin/Private/SIOJEditorPlugin.cpp b/Source/SIOJEditorPlugin/Private/SIOJEditorPlugin.cpp new file mode 100644 index 00000000..bbfa9b0a --- /dev/null +++ b/Source/SIOJEditorPlugin/Private/SIOJEditorPlugin.cpp @@ -0,0 +1,22 @@ +// Copyright 2015 Vladimir Alyamkin. All Rights Reserved. +// Original code by https://github.com/unktomi + +#include "SIOJEditorPluginPrivatePCH.h" +#include "SIOJEditorPlugin.h" + +#define LOCTEXT_NAMESPACE "FSIOJEditorPluginModule" + +void FSIOJEditorPluginModule::StartupModule() +{ + // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module +} + +void FSIOJEditorPluginModule::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FSIOJEditorPluginModule, SIOJEditorPlugin) diff --git a/Source/SIOJEditorPlugin/Private/SIOJEditorPluginPrivatePCH.h b/Source/SIOJEditorPlugin/Private/SIOJEditorPluginPrivatePCH.h new file mode 100644 index 00000000..a76f3887 --- /dev/null +++ b/Source/SIOJEditorPlugin/Private/SIOJEditorPluginPrivatePCH.h @@ -0,0 +1,16 @@ +// Copyright 2015 Vladimir Alyamkin. All Rights Reserved. +// Original code by https://github.com/unktomi + +#pragma once + +#include "KismetCompiler.h" +#include "EditorCategoryUtils.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNodeUtils.h" // for FNodeTextCache +#include "EdGraphSchema_K2.h" +#include "BlueprintNodeSpawner.h" +#include "BlueprintActionDatabaseRegistrar.h" +#include "BlueprintFieldNodeSpawner.h" +#include "EditorCategoryUtils.h" +#include "BlueprintActionFilter.h" + diff --git a/Source/SIOJEditorPlugin/Private/SIOJ_BreakJson.cpp b/Source/SIOJEditorPlugin/Private/SIOJ_BreakJson.cpp new file mode 100644 index 00000000..7ca8a013 --- /dev/null +++ b/Source/SIOJEditorPlugin/Private/SIOJ_BreakJson.cpp @@ -0,0 +1,284 @@ +// Copyright 2015 Vladimir Alyamkin. All Rights Reserved. +// Original code by https://github.com/unktomi + +#include "SIOJEditorPluginPrivatePCH.h" +#include "SIOJ_BreakJson.h" + +#include "Runtime/Launch/Resources/Version.h" + +#define LOCTEXT_NAMESPACE "SIOJ_BreakJson" + +class FKCHandler_BreakJson : public FNodeHandlingFunctor +{ + +public: + FKCHandler_BreakJson(FKismetCompilerContext& InCompilerContext) + : FNodeHandlingFunctor(InCompilerContext) + { + } + + virtual void Compile(FKismetFunctionContext& Context, UEdGraphNode* Node) override + { + UEdGraphPin* InputPin = NULL; + + for (int32 PinIndex = 0; PinIndex < Node->Pins.Num(); ++PinIndex) + { + UEdGraphPin* Pin = Node->Pins[PinIndex]; + if (Pin && (EGPD_Input == Pin->Direction)) + { + InputPin = Pin; + break; + } + } + + UEdGraphPin *InNet = FEdGraphUtilities::GetNetFromPin(InputPin); + UClass *Class = Cast(StaticLoadObject(UClass::StaticClass(), NULL, TEXT("class'SIOJPlugin.SIOJJsonObject'"))); + + FBPTerminal **SourceTerm = Context.NetMap.Find(InNet); + if (SourceTerm == nullptr) + { + return; + } + + for (int32 PinIndex = 0; PinIndex < Node->Pins.Num(); ++PinIndex) + { + UEdGraphPin* Pin = Node->Pins[PinIndex]; + if (Pin && (EGPD_Output == Pin->Direction)) + { + if (Pin->LinkedTo.Num() < 1) + { + continue; + } + + FBPTerminal **Target = Context.NetMap.Find(Pin); + + const FString &FieldName = Pin->PinName; + const FString &FieldType = Pin->PinType.PinCategory; + + FBPTerminal* FieldNameTerm = Context.CreateLocalTerminal(ETerminalSpecification::TS_Literal); + FieldNameTerm->Type.PinCategory = CompilerContext.GetSchema()->PC_String; +#if ENGINE_MINOR_VERSION >= 13 + FieldNameTerm->SourcePin = Pin; +#else + FieldNameTerm->Source = Pin; +#endif + FieldNameTerm->Name = FieldName; + FieldNameTerm->TextLiteral = FText::FromString(FieldName); + + FBlueprintCompiledStatement& Statement = Context.AppendStatementForNode(Node); + FName FunctionName; + + bool bIsArray = Pin->PinType.bIsArray; + + if (FieldType == CompilerContext.GetSchema()->PC_Boolean) + { + FunctionName = bIsArray ? TEXT("GetBoolArrayField") : TEXT("GetBoolField"); + } + else if (FieldType == CompilerContext.GetSchema()->PC_Float) + { + FunctionName = bIsArray ? TEXT("GetNumberArrayField") : TEXT("GetNumberField"); + } + else if (FieldType == CompilerContext.GetSchema()->PC_String) + { + FunctionName = bIsArray ? TEXT("GetStringArrayField") : TEXT("GetStringField"); + } + else if (FieldType == CompilerContext.GetSchema()->PC_Object) + { + FunctionName = bIsArray ? TEXT("GetObjectArrayField") : TEXT("GetObjectField"); + } + else + { + continue; + } + + UFunction *FunctionPtr = Class->FindFunctionByName(FunctionName); + Statement.Type = KCST_CallFunction; + Statement.FunctionToCall = FunctionPtr; + Statement.FunctionContext = *SourceTerm; + Statement.bIsParentContext = false; + Statement.LHS = *Target; + Statement.RHS.Add(FieldNameTerm); + } + } + } + + FBPTerminal* RegisterInputTerm(FKismetFunctionContext& Context, USIOJ_BreakJson* Node) + { + // Find input pin + UEdGraphPin* InputPin = NULL; + for (int32 PinIndex = 0; PinIndex < Node->Pins.Num(); ++PinIndex) + { + UEdGraphPin* Pin = Node->Pins[PinIndex]; + if (Pin && (EGPD_Input == Pin->Direction)) + { + InputPin = Pin; + break; + } + } + check(NULL != InputPin); + + // Find structure source net + UEdGraphPin* Net = FEdGraphUtilities::GetNetFromPin(InputPin); + FBPTerminal **TermPtr = Context.NetMap.Find(Net); + + if (!TermPtr) + { + FBPTerminal *Term = Context.CreateLocalTerminalFromPinAutoChooseScope(Net, Context.NetNameMap->MakeValidName(Net)); + + Context.NetMap.Add(Net, Term); + + return Term; + } + + return *TermPtr; + } + + void RegisterOutputTerm(FKismetFunctionContext& Context, UEdGraphPin* OutputPin, FBPTerminal* ContextTerm) + { + FBPTerminal *Term = Context.CreateLocalTerminalFromPinAutoChooseScope(OutputPin, Context.NetNameMap->MakeValidName(OutputPin)); + Context.NetMap.Add(OutputPin, Term); + } + + virtual void RegisterNets(FKismetFunctionContext& Context, UEdGraphNode* InNode) override + { + USIOJ_BreakJson* Node = Cast(InNode); + FNodeHandlingFunctor::RegisterNets(Context, Node); + + check(NULL != Node); + + if (FBPTerminal* StructContextTerm = RegisterInputTerm(Context, Node)) + { + for (int32 PinIndex = 0; PinIndex < Node->Pins.Num(); ++PinIndex) + { + UEdGraphPin* Pin = Node->Pins[PinIndex]; + if (NULL != Pin && EGPD_Output == Pin->Direction) + { + RegisterOutputTerm(Context, Pin, StructContextTerm); + } + } + } + } +}; + +/** + * Main node class + */ +USIOJ_BreakJson::USIOJ_BreakJson(const FObjectInitializer &ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +FNodeHandlingFunctor* USIOJ_BreakJson::CreateNodeHandler(class FKismetCompilerContext& CompilerContext) const +{ + return new FKCHandler_BreakJson(CompilerContext); +} + +void USIOJ_BreakJson::AllocateDefaultPins() +{ + const UEdGraphSchema_K2* K2Schema = GetDefault(); + + UClass *Class = Cast(StaticLoadObject(UClass::StaticClass(), NULL, TEXT("class'SIOJPlugin.SIOJJsonObject'"))); + UEdGraphPin* Pin = CreatePin(EGPD_Input, K2Schema->PC_Object, TEXT(""), Class, false, false, TEXT("Target")); + + K2Schema->SetPinDefaultValueBasedOnType(Pin); + + CreateProjectionPins(Pin); +} + +FLinearColor USIOJ_BreakJson::GetNodeTitleColor() const +{ + return FLinearColor(255.0f, 255.0f, 0.0f); +} + +void USIOJ_BreakJson::PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) +{ + bool bIsDirty = false; + + FName PropertyName = (PropertyChangedEvent.Property != NULL) ? PropertyChangedEvent.Property->GetFName() : NAME_None; + if (true || PropertyName == TEXT("Outputs")) + { + bIsDirty = true; + } + + if (bIsDirty) + { + ReconstructNode(); + GetGraph()->NotifyGraphChanged(); + } + + Super::PostEditChangeProperty(PropertyChangedEvent); +} + +void USIOJ_BreakJson::GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const +{ + // actions get registered under specific object-keys; the idea is that + // actions might have to be updated (or deleted) if their object-key is + // mutated (or removed)... here we use the node's class (so if the node + // type disappears, then the action should go with it) + UClass* ActionKey = GetClass(); + + // to keep from needlessly instantiating a UBlueprintNodeSpawner, first + // check to make sure that the registrar is looking for actions of this type + // (could be regenerating actions for a specific asset, and therefore the + // registrar would only accept actions corresponding to that asset) + if (ActionRegistrar.IsOpenForRegistration(ActionKey)) + { + UBlueprintNodeSpawner* NodeSpawner = UBlueprintNodeSpawner::Create(GetClass()); + check(NodeSpawner != nullptr); + + ActionRegistrar.AddBlueprintAction(ActionKey, NodeSpawner); + } +} + +FText USIOJ_BreakJson::GetMenuCategory() const +{ + static FNodeTextCache CachedCategory; + + if (CachedCategory.IsOutOfDate(this)) + { + // FText::Format() is slow, so we cache this to save on performance + CachedCategory.SetCachedText(FEditorCategoryUtils::BuildCategoryString(FCommonEditorCategory::Utilities, LOCTEXT("ActionMenuCategory", "Va Rest")), this); + } + return CachedCategory; +} + +void USIOJ_BreakJson::CreateProjectionPins(UEdGraphPin *Source) +{ + const UEdGraphSchema_K2* K2Schema = GetDefault(); + UClass *Class = Cast(StaticLoadObject(UClass::StaticClass(), NULL, TEXT("class'SIOJPlugin.SIOJJsonObject'"))); + + for (TArray::TIterator it(Outputs); it; ++it) + { + FString Type; + UObject *Subtype = nullptr; + FString FieldName = (*it).Name; + + switch ((*it).Type) { + case ESIOJ_JsonType::JSON_Bool: + Type = K2Schema->PC_Boolean; + break; + + case ESIOJ_JsonType::JSON_Number: + Type = K2Schema->PC_Float; + break; + + case ESIOJ_JsonType::JSON_String: + Type = K2Schema->PC_String; + break; + + case ESIOJ_JsonType::JSON_Object: + Type = K2Schema->PC_Object; + Subtype = Class; + break; + } + + UEdGraphPin *OutputPin = CreatePin(EGPD_Output, Type, TEXT(""), Subtype, (*it).bIsArray, false, (*it).Name); + } +} + +FText USIOJ_BreakJson::GetNodeTitle(ENodeTitleType::Type TitleType) const +{ + return LOCTEXT("SIOJ_Break_Json.NodeTitle", "Break Json"); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/SIOJEditorPlugin/Public/SIOJEditorPlugin.h b/Source/SIOJEditorPlugin/Public/SIOJEditorPlugin.h new file mode 100644 index 00000000..96aa0f25 --- /dev/null +++ b/Source/SIOJEditorPlugin/Public/SIOJEditorPlugin.h @@ -0,0 +1,16 @@ +// Copyright 2015 Vladimir Alyamkin. All Rights Reserved. +// Original code by https://github.com/unktomi + +#pragma once + +#include "ModuleManager.h" + +class FSIOJEditorPluginModule : public IModuleInterface +{ + +public: + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; + +}; diff --git a/Source/SIOJEditorPlugin/Public/SIOJ_BreakJson.h b/Source/SIOJEditorPlugin/Public/SIOJ_BreakJson.h new file mode 100644 index 00000000..857a81fc --- /dev/null +++ b/Source/SIOJEditorPlugin/Public/SIOJ_BreakJson.h @@ -0,0 +1,64 @@ +// Copyright 2015 Vladimir Alyamkin. All Rights Reserved. +// Original code by https://github.com/unktomi + +#pragma once + +#include "Engine.h" +#include "K2Node.h" + +#include "SIOJ_BreakJson.generated.h" + +UENUM(BlueprintType) +enum class ESIOJ_JsonType : uint8 +{ + //JSON_Null UMETA(DisplayName = "Null"), + JSON_Bool UMETA(DisplayName = "Boolean"), + JSON_Number UMETA(DisplayName = "Number"), + JSON_String UMETA(DisplayName = "String"), + JSON_Object UMETA(DisplayName = "Object") +}; + +USTRUCT(BlueprintType) +struct FSIOJ_NamedType +{ + GENERATED_USTRUCT_BODY(); + + UPROPERTY(EditAnywhere, Category = NamedType) + FString Name; + + UPROPERTY(EditAnywhere, Category = NamedType) + ESIOJ_JsonType Type; + + UPROPERTY(EditAnywhere, Category = NamedType) + bool bIsArray; +}; + +UCLASS(BlueprintType, Blueprintable) +class SIOJEDITORPLUGIN_API USIOJ_BreakJson : public UK2Node +{ + GENERATED_UCLASS_BODY() + +public: + // Begin UEdGraphNode interface. + virtual void AllocateDefaultPins() override; + virtual FLinearColor GetNodeTitleColor() const override; + virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override; + // End UEdGraphNode interface. + + // Begin UK2Node interface + virtual bool IsNodePure() const { return true; } + virtual bool ShouldShowNodeProperties() const { return true; } + void GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const override; + virtual FText GetMenuCategory() const override; + virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override; + virtual class FNodeHandlingFunctor* CreateNodeHandler(class FKismetCompilerContext& CompilerContext) const override; + // End UK2Node interface. + +protected: + virtual void CreateProjectionPins(UEdGraphPin *Source); + +public: + UPROPERTY(EditAnywhere, Category = PinOptions) + TArray Outputs; + +}; diff --git a/Source/SIOJEditorPlugin/SIOJEditorPlugin.Build.cs b/Source/SIOJEditorPlugin/SIOJEditorPlugin.Build.cs new file mode 100644 index 00000000..6eadbf84 --- /dev/null +++ b/Source/SIOJEditorPlugin/SIOJEditorPlugin.Build.cs @@ -0,0 +1,67 @@ +// Copyright 2015 Vladimir Alyamkin. All Rights Reserved. + +using UnrealBuildTool; + +public class SIOJEditorPlugin : ModuleRules +{ + public SIOJEditorPlugin(TargetInfo Target) + { + + PublicIncludePaths.AddRange( + new string[] { + "SIOJson", + "SIOJson/Public" + + // ... add public include paths required here ... + }); + + + PrivateIncludePaths.AddRange( + new string[] { + "SIOJEditorPlugin/Private", + + // ... add other private include paths required here ... + }); + + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "SIOJson" + + // ... add other public dependencies that you statically link with here ... + }); + + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "Slate", + "SlateCore", + "InputCore", + "AssetTools", + "UnrealEd", // for FAssetEditorManager + "KismetWidgets", + "KismetCompiler", + "BlueprintGraph", + "GraphEditor", + "Kismet", // for FWorkflowCentricApplication + "PropertyEditor", + "EditorStyle", + "Sequencer", + "DetailCustomizations", + "Settings", + "RenderCore" + }); + + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + // ... add any modules that your module loads dynamically here ... + }); + } +} diff --git a/Source/SIOJson/Private/SIOJConvert.cpp b/Source/SIOJson/Private/SIOJConvert.cpp new file mode 100644 index 00000000..ca25f156 --- /dev/null +++ b/Source/SIOJson/Private/SIOJConvert.cpp @@ -0,0 +1,402 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#include "SIOJsonPrivatePCH.h" + +typedef TJsonWriterFactory< TCHAR, TCondensedJsonPrintPolicy > FCondensedJsonStringWriterFactory; +typedef TJsonWriter< TCHAR, TCondensedJsonPrintPolicy > FCondensedJsonStringWriter; + +FString USIOJConvert::ToJsonString(const TSharedPtr& JsonObject) +{ + FString OutputString; + TSharedRef< FCondensedJsonStringWriter > Writer = FCondensedJsonStringWriterFactory::Create(&OutputString); + FJsonSerializer::Serialize(JsonObject.ToSharedRef(), Writer); + return OutputString; +} + +FString USIOJConvert::ToJsonString(const TArray>& JsonValueArray) +{ + FString OutputString; + TSharedRef< FCondensedJsonStringWriter > Writer = FCondensedJsonStringWriterFactory::Create(&OutputString); + FJsonSerializer::Serialize(JsonValueArray, Writer); + return OutputString; +} + +FString USIOJConvert::ToJsonString(const TSharedPtr& JsonValue) +{ + if (JsonValue->Type == EJson::None) + { + return FString(); + } + else if (JsonValue->Type == EJson::Null) + { + return FString(); + } + else if (JsonValue->Type == EJson::String) + { + return JsonValue->AsString(); + } + else if (JsonValue->Type == EJson::Number) + { + return FString::Printf(TEXT("%f"),JsonValue->AsNumber()); + } + else if (JsonValue->Type == EJson::Boolean) + { + return FString::Printf(TEXT("%d"), JsonValue->AsBool()); + } + else if (JsonValue->Type == EJson::Array) + { + return ToJsonString(JsonValue->AsArray()); + } + else if (JsonValue->Type == EJson::Object) + { + return ToJsonString(JsonValue->AsObject()); + } + else + { + return FString(); + } +} + +USIOJsonValue* USIOJConvert::ToSIOJsonValue(const TArray>& JsonValueArray) +{ + TArray< TSharedPtr > ValueArray; + for (auto InVal : JsonValueArray) + { + ValueArray.Add(InVal); + } + + USIOJsonValue* ResultValue = NewObject(); + TSharedPtr NewVal = MakeShareable(new FJsonValueArray(ValueArray)); + ResultValue->SetRootValue(NewVal); + + return ResultValue; +} + +#pragma endregion ToJsonValue + +TSharedPtr USIOJConvert::JsonStringToJsonValue(const FString& JsonString) +{ + //Null + if (JsonString.IsEmpty()) + { + return MakeShareable(new FJsonValueNull); + } + + //Number + if (JsonString.IsNumeric()) + { + //convert to double + return MakeShareable(new FJsonValueNumber(FCString::Atod(*JsonString))); + } + + //Object + if (JsonString.StartsWith(FString(TEXT("{")))) + { + TSharedPtr< FJsonObject > JsonObject = MakeShareable(new FJsonObject); + TSharedRef< TJsonReader<> > Reader = TJsonReaderFactory<>::Create(*JsonString); + bool success = FJsonSerializer::Deserialize(Reader, JsonObject); + + if (success) + { + return MakeShareable(new FJsonValueObject(JsonObject)); + } + } + + //Array + if (JsonString.StartsWith(FString(TEXT("[")))) + { + TArray < TSharedPtr> RawJsonValueArray; + TSharedRef< TJsonReader<> > Reader = TJsonReaderFactory<>::Create(*JsonString); + bool success = FJsonSerializer::Deserialize(Reader, RawJsonValueArray); + + if (success) + { + return MakeShareable(new FJsonValueArray(RawJsonValueArray)); + } + } + + //Bool + if (JsonString == FString("true") || JsonString == FString("false")) + { + bool BooleanValue = (JsonString == FString("true")); + return MakeShareable(new FJsonValueBoolean(BooleanValue)); + } + + //String + return MakeShareable(new FJsonValueString(JsonString)); +} + +TSharedPtr USIOJConvert::ToJsonValue(const TSharedPtr& JsonObject) +{ + return MakeShareable(new FJsonValueObject(JsonObject)); +} + +TSharedPtr USIOJConvert::ToJsonValue(const FString& StringValue) +{ + return MakeShareable(new FJsonValueString(StringValue)); +} + +TSharedPtr USIOJConvert::ToJsonValue(double NumberValue) +{ + return MakeShareable(new FJsonValueNumber(NumberValue)); +} + +TSharedPtr USIOJConvert::ToJsonValue(bool BoolValue) +{ + return MakeShareable(new FJsonValueBoolean(BoolValue)); +} + +TSharedPtr USIOJConvert::ToJsonValue(const TArray& BinaryValue) +{ + return MakeShareable(new FJsonValueBinary(BinaryValue)); +} + +TSharedPtr USIOJConvert::ToJsonValue(const TArray>& ArrayValue) +{ + return MakeShareable(new FJsonValueArray(ArrayValue)); +} + +#pragma endregion ToJsonValue + +TArray> USIOJConvert::JsonStringToJsonArray(const FString& JsonString) +{ + TArray < TSharedPtr> RawJsonValueArray; + TSharedRef< TJsonReader<> > Reader = TJsonReaderFactory<>::Create(*JsonString); + FJsonSerializer::Deserialize(Reader, RawJsonValueArray); + + return RawJsonValueArray; +} + +TSharedPtr USIOJConvert::ToJsonObject(const FString& JsonString) +{ + TSharedPtr< FJsonObject > JsonObject = MakeShareable(new FJsonObject); + TSharedRef< TJsonReader<> > Reader = TJsonReaderFactory<>::Create(*JsonString); + FJsonSerializer::Deserialize(Reader, JsonObject); + return JsonObject; +} + +TSharedPtr USIOJConvert::ToJsonObject(UStruct* Struct, void* StructPtr, bool IsBlueprintStruct) +{ + if (IsBlueprintStruct) + { + //Get the object keys + TSharedPtr Object = ToJsonObject(Struct, StructPtr, false); + + //Wrap it into a value and pass it into the trimmer + TSharedPtr JsonValue = MakeShareable(new FJsonValueObject(Object)); + TrimValueKeyNames(JsonValue); + + //Return object with trimmed names + return JsonValue->AsObject(); + } + else + { + TSharedRef JsonObject = MakeShareable(new FJsonObject); + bool success = FJsonObjectConverter::UStructToJsonObject(Struct, StructPtr, JsonObject, 0, 0); + return JsonObject; + } +} + +TSharedPtr USIOJConvert::MakeJsonObject() +{ + return MakeShareable(new FJsonObject); +} + +bool USIOJConvert::JsonObjectToUStruct(TSharedPtr JsonObject, UStruct* Struct, void* StructPtr, bool IsBlueprintStruct) +{ + if (IsBlueprintStruct) + { + //Json object we pass will have their trimmed BP names, e.g. boolKey vs boolKey_8_EDBB36654CF43866C376DE921373AF23 + //so we have to match them to the verbose versions, get a map of the names + + TSharedPtr KeyMap = MakeShareable(new FTrimmedKeyMap); + SetTrimmedKeyMapForStruct(KeyMap, Struct); + + //Adjust our passed in JsonObject to use the long key names + TSharedPtr JsonValue = MakeShareable(new FJsonValueObject(JsonObject)); + ReplaceJsonValueNamesWithMap(JsonValue, KeyMap); + + //Now it's a regular struct and will fill correctly + return JsonObjectToUStruct(JsonObject, Struct, StructPtr, false); + } + else + { + return FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), Struct, StructPtr, 0, 0); + } +} + +void USIOJConvert::TrimValueKeyNames(const TSharedPtr& JsonValue) +{ + //Array? + if (JsonValue->Type == EJson::Array) + { + auto Array = JsonValue->AsArray(); + + for (auto SubValue : Array) + { + TrimValueKeyNames(SubValue); + } + } + //Object? + else if (JsonValue->Type == EJson::Object) + { + auto JsonObject = JsonValue->AsObject(); + for (auto Pair : JsonObject->Values) + { + const FString& Key = Pair.Key; + FString TrimmedKey; + + bool DidNeedTrimming = TrimKey(Key, TrimmedKey); + + //Positive count? trim it + if (DidNeedTrimming) + { + //Trim subvalue if applicable + auto SubValue = Pair.Value; + TrimValueKeyNames(SubValue); + + JsonObject->SetField(TrimmedKey, SubValue); + JsonObject->RemoveField(Key); + + //UE_LOG(LogTemp, Log, TEXT("orig: %s, trimmed: %s"), *Pair.Key, *TrimmedKey); + } + else + { + //UE_LOG(LogTemp, Log, TEXT("untrimmed: %s"), *Pair.Key); + } + } + } +} + +bool USIOJConvert::TrimKey(const FString& InLongKey, FString& OutTrimmedKey) +{ + //Look for the position of the 2nd '_' + int32 LastIndex = InLongKey.Find(TEXT("_"), ESearchCase::IgnoreCase, ESearchDir::FromEnd); + LastIndex = InLongKey.Find(TEXT("_"), ESearchCase::IgnoreCase, ESearchDir::FromEnd, LastIndex); + + if (LastIndex >= 0) + { + OutTrimmedKey = InLongKey.Mid(0, LastIndex);; + return true; + } + else + { + return false; + } +} + + + +void USIOJConvert::SetTrimmedKeyMapForStruct(TSharedPtr& InMap, UStruct* Struct) +{ + //Get the child fields + auto FieldPtr = Struct->Children; + + //If it hasn't been set, the long key is the json standardized long name + if (InMap->LongKey.IsEmpty()) + { + InMap->LongKey = FJsonObjectConverter::StandardizeCase(Struct->GetName()); + } + + //For each child field... + while (FieldPtr != NULL) { + //Map our trimmed name to our full name + const FString& LowerKey = FJsonObjectConverter::StandardizeCase(FieldPtr->GetName()); + FString TrimmedKey; + bool DidTrim = TrimKey(LowerKey, TrimmedKey); + + //Set the key map + TSharedPtr SubMap = MakeShareable(new FTrimmedKeyMap); + SubMap->LongKey = LowerKey; + + //No-trim case, trim = long + if (!DidTrim) + { + TrimmedKey = SubMap->LongKey; + } + + //Did we get a substructure? + UStructProperty* SubStruct = Cast(FieldPtr); + if (SubStruct != NULL) + { + //We did, embed the sub-map + SetTrimmedKeyMapForStruct(SubMap, SubStruct->Struct); + } + + //Did we get a sub-array? + UArrayProperty* ArrayProp = Cast(FieldPtr); + if (ArrayProp != NULL) + { + //set the inner map for the inner property + SetTrimmedKeyMapForProp(SubMap, ArrayProp->Inner); + + //UE_LOG(LogTemp, Log, TEXT("found array: %s"), *ArrayProp->GetName()); + } + + InMap->SubMap.Add(TrimmedKey, SubMap); + //UE_LOG(LogTemp, Log, TEXT("long: %s, trim: %s, is struct: %d"), *SubMap->LongKey, *TrimmedKey, SubStruct != NULL); + + FieldPtr = FieldPtr->Next; + } + + //UE_LOG(LogTemp, Log, TEXT("Final map: %d"), InMap->SubMap.Num()); +} + +void USIOJConvert::SetTrimmedKeyMapForProp(TSharedPtr& InMap, UProperty* InnerProperty) +{ + + //UE_LOG(LogTemp, Log, TEXT("got prop: %s"), *InnerProperty->GetName()); + UStructProperty* SubStruct = Cast(InnerProperty); + if (SubStruct != NULL) + { + //We did, embed the sub-map + SetTrimmedKeyMapForStruct(InMap, SubStruct->Struct); + } + + //Did we get a sub-array? + UArrayProperty* ArrayProp = Cast(InnerProperty); + if (ArrayProp != NULL) + { + SetTrimmedKeyMapForProp(InMap, ArrayProp->Inner); + + } +} + +void USIOJConvert::ReplaceJsonValueNamesWithMap(TSharedPtr& JsonValue, TSharedPtr KeyMap) +{ + if (JsonValue->Type == EJson::Object) + { + //Go through each key in the object + auto Object = JsonValue->AsObject(); + auto SubMap = KeyMap->SubMap; + auto AllValues = Object->Values; + + for (auto Pair : AllValues) + { + if (SubMap.Num() > 0) + { + //Get the long key for entry + const FString& LongKey = SubMap[Pair.Key]->LongKey; + + //loop nested structures + ReplaceJsonValueNamesWithMap(Pair.Value, SubMap[Pair.Key]); + + if (Pair.Key != LongKey) + { + //finally set the field and remove the old field + Object->SetField(LongKey, Pair.Value); + Object->RemoveField(Pair.Key); + } + } + } + } + else if (JsonValue->Type == EJson::Array) + { + auto Array = JsonValue->AsArray(); + for (auto Item : Array) + { + //UE_LOG(LogTemp, Log, TEXT("%s"), *Item->AsString()); + ReplaceJsonValueNamesWithMap(Item, KeyMap); + } + } +} \ No newline at end of file diff --git a/Source/SIOJson/Private/SIOJLibrary.cpp b/Source/SIOJson/Private/SIOJLibrary.cpp new file mode 100644 index 00000000..c2f7b1e8 --- /dev/null +++ b/Source/SIOJson/Private/SIOJLibrary.cpp @@ -0,0 +1,204 @@ +// Copyright 2016 Vladimir Alyamkin. All Rights Reserved. + +#include "SIOJsonPrivatePCH.h" +#include "Base64.h" + +////////////////////////////////////////////////////////////////////////// +// Helpers + +FString USIOJLibrary::PercentEncode(const FString& Source) +{ + FString OutText = Source; + + OutText = OutText.Replace(TEXT(" "), TEXT("%20")); + OutText = OutText.Replace(TEXT("!"), TEXT("%21")); + OutText = OutText.Replace(TEXT("\""), TEXT("%22")); + OutText = OutText.Replace(TEXT("#"), TEXT("%23")); + OutText = OutText.Replace(TEXT("$"), TEXT("%24")); + OutText = OutText.Replace(TEXT("&"), TEXT("%26")); + OutText = OutText.Replace(TEXT("'"), TEXT("%27")); + OutText = OutText.Replace(TEXT("("), TEXT("%28")); + OutText = OutText.Replace(TEXT(")"), TEXT("%29")); + OutText = OutText.Replace(TEXT("*"), TEXT("%2A")); + OutText = OutText.Replace(TEXT("+"), TEXT("%2B")); + OutText = OutText.Replace(TEXT(","), TEXT("%2C")); + OutText = OutText.Replace(TEXT("/"), TEXT("%2F")); + OutText = OutText.Replace(TEXT(":"), TEXT("%3A")); + OutText = OutText.Replace(TEXT(";"), TEXT("%3B")); + OutText = OutText.Replace(TEXT("="), TEXT("%3D")); + OutText = OutText.Replace(TEXT("?"), TEXT("%3F")); + OutText = OutText.Replace(TEXT("@"), TEXT("%40")); + OutText = OutText.Replace(TEXT("["), TEXT("%5B")); + OutText = OutText.Replace(TEXT("]"), TEXT("%5D")); + OutText = OutText.Replace(TEXT("{"), TEXT("%7B")); + OutText = OutText.Replace(TEXT("}"), TEXT("%7D")); + + return OutText; +} + +FString USIOJLibrary::Base64Encode(const FString& Source) +{ + return FBase64::Encode(Source); +} + +bool USIOJLibrary::Base64Decode(const FString& Source, FString& Dest) +{ + return FBase64::Decode(Source, Dest); +} + + +bool USIOJLibrary::StringToJsonValueArray(const FString& JsonString, TArray& OutJsonValueArray) +{ + TArray < TSharedPtr> RawJsonValueArray; + TSharedRef< TJsonReader<> > Reader = TJsonReaderFactory<>::Create(*JsonString); + FJsonSerializer::Deserialize(Reader, RawJsonValueArray); + + for (auto Value : RawJsonValueArray) + { + auto SJsonValue = NewObject(); + SJsonValue->SetRootValue(Value); + OutJsonValueArray.Add(SJsonValue); + } + + return OutJsonValueArray.Num() > 0; +} + +////////////////////////////////////////////////////////////////////////// +// Easy URL processing + +TMap USIOJLibrary::RequestMap; + + +FString USIOJLibrary::Conv_JsonObjectToString(USIOJsonObject* InObject) +{ + return InObject->EncodeJson(); +} + + +USIOJsonObject* USIOJLibrary::Conv_JsonValueToJsonObject(class USIOJsonValue* InValue) +{ + return InValue->AsObject(); +} + +USIOJsonValue* USIOJLibrary::Conv_ArrayToJsonValue(const TArray& InArray) +{ + return USIOJsonValue::ConstructJsonValueArray(nullptr, InArray); +} + + +USIOJsonValue* USIOJLibrary::Conv_JsonObjectToJsonValue(USIOJsonObject* InObject) +{ + return USIOJsonValue::ConstructJsonValueObject(InObject, nullptr); +} + + +USIOJsonValue* USIOJLibrary::Conv_BytesToJsonValue(const TArray& InBytes) +{ + return USIOJsonValue::ConstructJsonValueBinary(nullptr, InBytes); +} + + +USIOJsonValue* USIOJLibrary::Conv_StringToJsonValue(const FString& InString) +{ + return USIOJsonValue::ConstructJsonValueString(nullptr, InString); +} + + +USIOJsonValue* USIOJLibrary::Conv_IntToJsonValue(int32 InInt) +{ + TSharedPtr NewVal = MakeShareable(new FJsonValueNumber(InInt)); + + USIOJsonValue* NewValue = NewObject(); + NewValue->SetRootValue(NewVal); + + return NewValue; +} + +USIOJsonValue* USIOJLibrary::Conv_FloatToJsonValue(float InFloat) +{ + return USIOJsonValue::ConstructJsonValueNumber(nullptr, InFloat); +} + + +USIOJsonValue* USIOJLibrary::Conv_BoolToJsonValue(bool InBool) +{ + return USIOJsonValue::ConstructJsonValueBool(nullptr, InBool); +} + + +int32 USIOJLibrary::Conv_JsonValueToInt(USIOJsonValue* InValue) +{ + return (int32)InValue->AsNumber(); +} + + +float USIOJLibrary::Conv_JsonValueToFloat(USIOJsonValue* InValue) +{ + return InValue->AsNumber(); +} + + +bool USIOJLibrary::Conv_JsonValueToBool(USIOJsonValue* InValue) +{ + return InValue->AsBool(); +} + + +TArray USIOJLibrary::Conv_JsonValueToBytes(USIOJsonValue* InValue) +{ + return InValue->AsBinary(); +} + +void USIOJLibrary::CallURL(UObject* WorldContextObject, const FString& URL, ERequestVerb Verb, ERequestContentType ContentType, USIOJsonObject* SIOJJson, const FSIOJCallDelegate& Callback) +{ + UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject); + if (World == nullptr) + { + UE_LOG(LogSIOJ, Error, TEXT("USIOJLibrary: Wrong world context")) + return; + } + + // Check we have valid data json + if (SIOJJson == nullptr) + { + SIOJJson = USIOJsonObject::ConstructJsonObject(WorldContextObject); + } + + USIOJRequestJSON* Request = NewObject(); + + Request->SetVerb(Verb); + Request->SetContentType(ContentType); + Request->SetRequestObject(SIOJJson); + + FSIOJCallResponse Response; + Response.Request = Request; + Response.WorldContextObject = WorldContextObject; + Response.Callback = Callback; + + Response.CompleteDelegateHandle = Request->OnStaticRequestComplete.AddStatic(&USIOJLibrary::OnCallComplete); + Response.FailDelegateHandle = Request->OnStaticRequestFail.AddStatic(&USIOJLibrary::OnCallComplete); + + RequestMap.Add(Request, Response); + + Request->ResetResponseData(); + Request->ProcessURL(URL); +} + +void USIOJLibrary::OnCallComplete(USIOJRequestJSON* Request) +{ + if (!RequestMap.Contains(Request)) + { + return; + } + + FSIOJCallResponse* Response = RequestMap.Find(Request); + + Request->OnStaticRequestComplete.Remove(Response->CompleteDelegateHandle); + Request->OnStaticRequestFail.Remove(Response->FailDelegateHandle); + + Response->Callback.ExecuteIfBound(Request); + + Response->WorldContextObject = nullptr; + Response->Request = nullptr; + RequestMap.Remove(Request); +} diff --git a/Source/SIOJson/Private/SIOJRequestJSON.cpp b/Source/SIOJson/Private/SIOJRequestJSON.cpp new file mode 100644 index 00000000..fa889638 --- /dev/null +++ b/Source/SIOJson/Private/SIOJRequestJSON.cpp @@ -0,0 +1,450 @@ +// Copyright 2014 Vladimir Alyamkin. All Rights Reserved. + +#include "SIOJsonPrivatePCH.h" +#include "CoreMisc.h" + +template void FSIOJLatentAction::Cancel() +{ + UObject *Obj = Request.Get(); + if (Obj != nullptr) + { + ((USIOJRequestJSON*)Obj)->Cancel(); + } +} + +USIOJRequestJSON::USIOJRequestJSON(const class FObjectInitializer& PCIP) + : Super(PCIP), + BinaryContentType(TEXT("application/octet-stream")) +{ + RequestVerb = ERequestVerb::GET; + RequestContentType = ERequestContentType::x_www_form_urlencoded_url; + + ResetData(); +} + +USIOJRequestJSON* USIOJRequestJSON::ConstructRequest(UObject* WorldContextObject) +{ + return NewObject(); +} + +USIOJRequestJSON* USIOJRequestJSON::ConstructRequestExt( + UObject* WorldContextObject, + ERequestVerb Verb, + ERequestContentType ContentType) +{ + USIOJRequestJSON* Request = ConstructRequest(WorldContextObject); + + Request->SetVerb(Verb); + Request->SetContentType(ContentType); + + return Request; +} + +void USIOJRequestJSON::SetVerb(ERequestVerb Verb) +{ + RequestVerb = Verb; +} + +void USIOJRequestJSON::SetCustomVerb(FString Verb) +{ + CustomVerb = Verb; +} + +void USIOJRequestJSON::SetContentType(ERequestContentType ContentType) +{ + RequestContentType = ContentType; +} + +void USIOJRequestJSON::SetBinaryContentType(const FString &ContentType) +{ + BinaryContentType = ContentType; +} + +void USIOJRequestJSON::SetBinaryRequestContent(const TArray &Bytes) +{ + RequestBytes = Bytes; +} + +void USIOJRequestJSON::SetHeader(const FString& HeaderName, const FString& HeaderValue) +{ + RequestHeaders.Add(HeaderName, HeaderValue); +} + + +////////////////////////////////////////////////////////////////////////// +// Destruction and reset + +void USIOJRequestJSON::ResetData() +{ + ResetRequestData(); + ResetResponseData(); +} + +void USIOJRequestJSON::ResetRequestData() +{ + if (RequestJsonObj != nullptr) + { + RequestJsonObj->Reset(); + } + else + { + RequestJsonObj = NewObject(); + } + + HttpRequest = FHttpModule::Get().CreateRequest(); +} + +void USIOJRequestJSON::ResetResponseData() +{ + if (ResponseJsonObj != nullptr) + { + ResponseJsonObj->Reset(); + } + else + { + ResponseJsonObj = NewObject(); + } + + ResponseHeaders.Empty(); + ResponseCode = -1; + + bIsValidJsonResponse = false; +} + +void USIOJRequestJSON::Cancel() +{ + ContinueAction = nullptr; + + ResetResponseData(); +} + + +////////////////////////////////////////////////////////////////////////// +// JSON data accessors + +USIOJsonObject* USIOJRequestJSON::GetRequestObject() +{ + return RequestJsonObj; +} + +void USIOJRequestJSON::SetRequestObject(USIOJsonObject* JsonObject) +{ + RequestJsonObj = JsonObject; +} + +USIOJsonObject* USIOJRequestJSON::GetResponseObject() +{ + return ResponseJsonObj; +} + +void USIOJRequestJSON::SetResponseObject(USIOJsonObject* JsonObject) +{ + ResponseJsonObj = JsonObject; +} + + +/////////////////////////////////////////////////////////////////////////// +// Response data access + +FString USIOJRequestJSON::GetURL() +{ + return HttpRequest->GetURL(); +} + +ERequestStatus USIOJRequestJSON::GetStatus() +{ + return ERequestStatus((uint8)HttpRequest->GetStatus()); +} + +int32 USIOJRequestJSON::GetResponseCode() +{ + return ResponseCode; +} + +FString USIOJRequestJSON::GetResponseHeader(const FString HeaderName) +{ + FString Result; + + FString* Header = ResponseHeaders.Find(HeaderName); + if (Header != nullptr) + { + Result = *Header; + } + + return Result; +} + +TArray USIOJRequestJSON::GetAllResponseHeaders() +{ + TArray Result; + for (TMap::TConstIterator It(ResponseHeaders); It; ++It) + { + Result.Add(It.Key() + TEXT(": ") + It.Value()); + } + return Result; +} + + +////////////////////////////////////////////////////////////////////////// +// URL processing + +void USIOJRequestJSON::ProcessURL(const FString& Url) +{ + HttpRequest->SetURL(Url); + + ProcessRequest(); +} + +void USIOJRequestJSON::ApplyURL(const FString& Url, USIOJsonObject *&Result, UObject* WorldContextObject, FLatentActionInfo LatentInfo) +{ + HttpRequest->SetURL(Url); + + // Prepare latent action + if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject)) + { + FLatentActionManager& LatentActionManager = World->GetLatentActionManager(); + FSIOJLatentAction *Kont = LatentActionManager.FindExistingAction>(LatentInfo.CallbackTarget, LatentInfo.UUID); + + if (Kont != nullptr) + { + Kont->Cancel(); + LatentActionManager.RemoveActionsForObject(LatentInfo.CallbackTarget); + } + + LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, ContinueAction = new FSIOJLatentAction(this, Result, LatentInfo)); + } + + ProcessRequest(); +} + +void USIOJRequestJSON::ProcessRequest() +{ + // Set verb + switch (RequestVerb) + { + case ERequestVerb::GET: + HttpRequest->SetVerb(TEXT("GET")); + break; + + case ERequestVerb::POST: + HttpRequest->SetVerb(TEXT("POST")); + break; + + case ERequestVerb::PUT: + HttpRequest->SetVerb(TEXT("PUT")); + break; + + case ERequestVerb::DEL: + HttpRequest->SetVerb(TEXT("DELETE")); + break; + + case ERequestVerb::CUSTOM: + HttpRequest->SetVerb(CustomVerb); + break; + + default: + break; + } + + // Set content-type + switch (RequestContentType) + { + case ERequestContentType::x_www_form_urlencoded_url: + { + HttpRequest->SetHeader(TEXT("Content-Type"), TEXT("application/x-www-form-urlencoded")); + + FString UrlParams = ""; + uint16 ParamIdx = 0; + + // Loop through all the values and prepare additional url part + for (auto RequestIt = RequestJsonObj->GetRootObject()->Values.CreateIterator(); RequestIt; ++RequestIt) + { + FString Key = RequestIt.Key(); + FString Value = RequestIt.Value().Get()->AsString(); + + if (!Key.IsEmpty() && !Value.IsEmpty()) + { + UrlParams += ParamIdx == 0 ? "?" : "&"; + UrlParams += USIOJLibrary::PercentEncode(Key) + "=" + USIOJLibrary::PercentEncode(Value); + } + + ParamIdx++; + } + + // Apply params + HttpRequest->SetURL(HttpRequest->GetURL() + UrlParams); + + UE_LOG(LogSIOJ, Log, TEXT("Request (urlencoded): %s %s %s"), *HttpRequest->GetVerb(), *HttpRequest->GetURL(), *UrlParams); + + break; + } + case ERequestContentType::x_www_form_urlencoded_body: + { + HttpRequest->SetHeader(TEXT("Content-Type"), TEXT("application/x-www-form-urlencoded")); + + FString UrlParams = ""; + uint16 ParamIdx = 0; + + // Loop through all the values and prepare additional url part + for (auto RequestIt = RequestJsonObj->GetRootObject()->Values.CreateIterator(); RequestIt; ++RequestIt) + { + FString Key = RequestIt.Key(); + FString Value = RequestIt.Value().Get()->AsString(); + + if (!Key.IsEmpty() && !Value.IsEmpty()) + { + UrlParams += ParamIdx == 0 ? "" : "&"; + UrlParams += USIOJLibrary::PercentEncode(Key) + "=" + USIOJLibrary::PercentEncode(Value); + } + + ParamIdx++; + } + + // Apply params + HttpRequest->SetContentAsString(UrlParams); + + UE_LOG(LogSIOJ, Log, TEXT("Request (url body): %s %s %s"), *HttpRequest->GetVerb(), *HttpRequest->GetURL(), *UrlParams); + + break; + } + case ERequestContentType::binary: + { + HttpRequest->SetHeader(TEXT("Content-Type"), BinaryContentType); + HttpRequest->SetContent(RequestBytes); + + UE_LOG(LogSIOJ, Log, TEXT("Request (binary): %s %s"), *HttpRequest->GetVerb(), *HttpRequest->GetURL()); + + break; + } + case ERequestContentType::json: + { + HttpRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); + + // Serialize data to json string + FString OutputString; + TSharedRef< TJsonWriter<> > Writer = TJsonWriterFactory<>::Create(&OutputString); + FJsonSerializer::Serialize(RequestJsonObj->GetRootObject().ToSharedRef(), Writer); + + // Set Json content + HttpRequest->SetContentAsString(OutputString); + + UE_LOG(LogSIOJ, Log, TEXT("Request (json): %s %s %s"), *HttpRequest->GetVerb(), *HttpRequest->GetURL(), *OutputString); + + break; + } + + default: + break; + } + + // Apply additional headers + for (TMap::TConstIterator It(RequestHeaders); It; ++It) + { + HttpRequest->SetHeader(It.Key(), It.Value()); + } + + // Bind event + HttpRequest->OnProcessRequestComplete().BindUObject(this, &USIOJRequestJSON::OnProcessRequestComplete); + + // Execute the request + HttpRequest->ProcessRequest(); +} + + +////////////////////////////////////////////////////////////////////////// +// Request callbacks + +void USIOJRequestJSON::OnProcessRequestComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) +{ + // Be sure that we have no data from previous response + ResetResponseData(); + + // Check we have a response and save response code as int32 + if(Response.IsValid()) + { + ResponseCode = Response->GetResponseCode(); + } + + // Check we have result to process futher + if (!bWasSuccessful || !Response.IsValid()) + { + UE_LOG(LogSIOJ, Error, TEXT("Request failed (%d): %s"), ResponseCode, *Request->GetURL()); + + // Broadcast the result event + OnRequestFail.Broadcast(this); + OnStaticRequestFail.Broadcast(this); + + return; + } + + // Save response data as a string + ResponseContent = Response->GetContentAsString(); + + // Log response state + UE_LOG(LogSIOJ, Log, TEXT("Response (%d): %s"), ResponseCode, *ResponseContent); + + // Process response headers + TArray Headers = Response->GetAllHeaders(); + for (FString Header : Headers) + { + FString Key; + FString Value; + if (Header.Split(TEXT(": "), &Key, &Value)) + { + ResponseHeaders.Add(Key, Value); + } + } + + // Try to deserialize data to JSON + TSharedRef> JsonReader = TJsonReaderFactory::Create(ResponseContent); + FJsonSerializer::Deserialize(JsonReader, ResponseJsonObj->GetRootObject()); + + // Decide whether the request was successful + bIsValidJsonResponse = bWasSuccessful && ResponseJsonObj->GetRootObject().IsValid(); + + // Log errors + if (!bIsValidJsonResponse) + { + if (!ResponseJsonObj->GetRootObject().IsValid()) + { + // As we assume it's recommended way to use current class, but not the only one, + // it will be the warning instead of error + UE_LOG(LogSIOJ, Warning, TEXT("JSON could not be decoded!")); + } + } + + // Broadcast the result event + OnRequestComplete.Broadcast(this); + OnStaticRequestComplete.Broadcast(this); + + // Finish the latent action + if (ContinueAction) + { + FSIOJLatentAction *K = ContinueAction; + ContinueAction = nullptr; + + K->Call(ResponseJsonObj); + } +} + + +////////////////////////////////////////////////////////////////////////// +// Tags + +void USIOJRequestJSON::AddTag(FName Tag) +{ + if (Tag != NAME_None) + { + Tags.AddUnique(Tag); + } +} + +int32 USIOJRequestJSON::RemoveTag(FName Tag) +{ + return Tags.Remove(Tag); +} + +bool USIOJRequestJSON::HasTag(FName Tag) const +{ + return (Tag != NAME_None) && Tags.Contains(Tag); +} diff --git a/Source/SIOJson/Private/SIOJson.cpp b/Source/SIOJson/Private/SIOJson.cpp new file mode 100644 index 00000000..d1772f49 --- /dev/null +++ b/Source/SIOJson/Private/SIOJson.cpp @@ -0,0 +1,24 @@ +// Copyright 2014 Vladimir Alyamkin. All Rights Reserved. + +#include "SIOJsonPrivatePCH.h" + +class FSIOJson : public ISIOJson +{ + /** IModuleInterface implementation */ + virtual void StartupModule() override + { + // @HACK Force classes to be compiled on shipping build + USIOJsonObject::StaticClass(); + USIOJsonValue::StaticClass(); + USIOJRequestJSON::StaticClass(); + } + + virtual void ShutdownModule() override + { + + } +}; + +IMPLEMENT_MODULE(FSIOJson, SIOJson) + +DEFINE_LOG_CATEGORY(LogSIOJ); diff --git a/Source/SIOJson/Private/SIOJsonObject.cpp b/Source/SIOJson/Private/SIOJsonObject.cpp new file mode 100644 index 00000000..492ccd49 --- /dev/null +++ b/Source/SIOJson/Private/SIOJsonObject.cpp @@ -0,0 +1,549 @@ +// Copyright 2014 Vladimir Alyamkin. All Rights Reserved. + +#include "SIOJsonPrivatePCH.h" + +typedef TJsonWriterFactory< TCHAR, TCondensedJsonPrintPolicy > FCondensedJsonStringWriterFactory; +typedef TJsonWriter< TCHAR, TCondensedJsonPrintPolicy > FCondensedJsonStringWriter; + +USIOJsonObject::USIOJsonObject(const class FObjectInitializer& PCIP) + : Super(PCIP) +{ + Reset(); +} + +USIOJsonObject* USIOJsonObject::ConstructJsonObject(UObject* WorldContextObject) +{ + return NewObject(); +} + +void USIOJsonObject::Reset() +{ + if (JsonObj.IsValid()) + { + JsonObj.Reset(); + } + + JsonObj = MakeShareable(new FJsonObject()); +} + +TSharedPtr& USIOJsonObject::GetRootObject() +{ + return JsonObj; +} + +void USIOJsonObject::SetRootObject(TSharedPtr& JsonObject) +{ + JsonObj = JsonObject; +} + + +////////////////////////////////////////////////////////////////////////// +// Serialization + +FString USIOJsonObject::EncodeJson() const +{ + if (!JsonObj.IsValid()) + { + return TEXT(""); + } + + FString OutputString; + TSharedRef< FCondensedJsonStringWriter > Writer = FCondensedJsonStringWriterFactory::Create(&OutputString); + FJsonSerializer::Serialize(JsonObj.ToSharedRef(), Writer); + + return OutputString; +} + +FString USIOJsonObject::EncodeJsonToSingleString() const +{ + FString OutputString = EncodeJson(); + + // Remove line terminators + OutputString.Replace(LINE_TERMINATOR, TEXT("")); + + // Remove tabs + OutputString.Replace(LINE_TERMINATOR, TEXT("\t")); + + return OutputString; +} + +bool USIOJsonObject::DecodeJson(const FString& JsonString) +{ + TSharedRef< TJsonReader<> > Reader = TJsonReaderFactory<>::Create(*JsonString); + if (FJsonSerializer::Deserialize(Reader, JsonObj) && JsonObj.IsValid()) + { + return true; + } + + // If we've failed to deserialize the string, we should clear our internal data + Reset(); + + UE_LOG(LogSIOJ, Error, TEXT("Json decoding failed for: %s"), *JsonString); + + return false; +} + + +////////////////////////////////////////////////////////////////////////// +// FJsonObject API + +TArray USIOJsonObject::GetFieldNames() +{ + TArray Result; + + if (!JsonObj.IsValid()) + { + return Result; + } + + JsonObj->Values.GetKeys(Result); + + return Result; +} + +bool USIOJsonObject::HasField(const FString& FieldName) const +{ + if (!JsonObj.IsValid() || FieldName.IsEmpty()) + { + return false; + } + + return JsonObj->HasField(FieldName); +} + +void USIOJsonObject::RemoveField(const FString& FieldName) +{ + if (!JsonObj.IsValid() || FieldName.IsEmpty()) + { + return; + } + + JsonObj->RemoveField(FieldName); +} + +USIOJsonValue* USIOJsonObject::GetField(const FString& FieldName) const +{ + if (!JsonObj.IsValid() || FieldName.IsEmpty()) + { + return nullptr; + } + + TSharedPtr NewVal = JsonObj->TryGetField(FieldName); + if (NewVal.IsValid()) + { + USIOJsonValue* NewValue = NewObject(); + NewValue->SetRootValue(NewVal); + + return NewValue; + } + + return nullptr; +} + +void USIOJsonObject::SetField(const FString& FieldName, USIOJsonValue* JsonValue) +{ + if (!JsonObj.IsValid() || FieldName.IsEmpty()) + { + return; + } + + JsonObj->SetField(FieldName, JsonValue->GetRootValue()); +} + + +////////////////////////////////////////////////////////////////////////// +// FJsonObject API Helpers (easy to use with simple Json objects) + +float USIOJsonObject::GetNumberField(const FString& FieldName) const +{ + if (!JsonObj.IsValid() || !JsonObj->HasTypedField(FieldName)) + { + UE_LOG(LogSIOJ, Warning, TEXT("No field with name %s of type Number"), *FieldName); + return 0.0f; + } + + return JsonObj->GetNumberField(FieldName); +} + +void USIOJsonObject::SetNumberField(const FString& FieldName, float Number) +{ + if (!JsonObj.IsValid() || FieldName.IsEmpty()) + { + return; + } + + JsonObj->SetNumberField(FieldName, Number); +} + +FString USIOJsonObject::GetStringField(const FString& FieldName) const +{ + if (!JsonObj.IsValid() || !JsonObj->HasTypedField(FieldName)) + { + UE_LOG(LogSIOJ, Warning, TEXT("No field with name %s of type String"), *FieldName); + return TEXT(""); + } + + return JsonObj->GetStringField(FieldName); +} + +void USIOJsonObject::SetStringField(const FString& FieldName, const FString& StringValue) +{ + if (!JsonObj.IsValid() || FieldName.IsEmpty()) + { + return; + } + + JsonObj->SetStringField(FieldName, StringValue); +} + +bool USIOJsonObject::GetBoolField(const FString& FieldName) const +{ + if (!JsonObj.IsValid() || !JsonObj->HasTypedField(FieldName)) + { + UE_LOG(LogSIOJ, Warning, TEXT("No field with name %s of type Boolean"), *FieldName); + return false; + } + + return JsonObj->GetBoolField(FieldName); +} + +void USIOJsonObject::SetBoolField(const FString& FieldName, bool InValue) +{ + if (!JsonObj.IsValid() || FieldName.IsEmpty()) + { + return; + } + + JsonObj->SetBoolField(FieldName, InValue); +} + +TArray USIOJsonObject::GetArrayField(const FString& FieldName) +{ + if (!JsonObj->HasTypedField(FieldName)) + { + UE_LOG(LogSIOJ, Warning, TEXT("No field with name %s of type Array"), *FieldName); + } + + TArray OutArray; + if (!JsonObj.IsValid() || FieldName.IsEmpty()) + { + return OutArray; + } + + TArray< TSharedPtr > ValArray = JsonObj->GetArrayField(FieldName); + for (auto Value : ValArray) + { + USIOJsonValue* NewValue = NewObject(); + NewValue->SetRootValue(Value); + + OutArray.Add(NewValue); + } + + return OutArray; +} + +void USIOJsonObject::SetArrayField(const FString& FieldName, const TArray& InArray) +{ + if (!JsonObj.IsValid() || FieldName.IsEmpty()) + { + return; + } + + TArray< TSharedPtr > ValArray; + + // Process input array and COPY original values + for (auto InVal : InArray) + { + TSharedPtr JsonVal = InVal->GetRootValue(); + + switch (InVal->GetType()) + { + case ESIOJson::None: + break; + + case ESIOJson::Null: + ValArray.Add(MakeShareable(new FJsonValueNull())); + break; + + case ESIOJson::String: + ValArray.Add(MakeShareable(new FJsonValueString(JsonVal->AsString()))); + break; + + case ESIOJson::Number: + ValArray.Add(MakeShareable(new FJsonValueNumber(JsonVal->AsNumber()))); + break; + + case ESIOJson::Boolean: + ValArray.Add(MakeShareable(new FJsonValueBoolean(JsonVal->AsBool()))); + break; + + case ESIOJson::Array: + ValArray.Add(MakeShareable(new FJsonValueArray(JsonVal->AsArray()))); + break; + + case ESIOJson::Object: + ValArray.Add(MakeShareable(new FJsonValueObject(JsonVal->AsObject()))); + break; + + default: + break; + } + } + + JsonObj->SetArrayField(FieldName, ValArray); +} + +void USIOJsonObject::MergeJsonObject(USIOJsonObject* InJsonObject, bool Overwrite) +{ + TArray Keys = InJsonObject->GetFieldNames(); + + for (auto Key : Keys) + { + if (Overwrite == false && HasField(Key)) + { + continue; + } + + SetField(Key, InJsonObject->GetField(Key)); + } +} + +USIOJsonObject* USIOJsonObject::GetObjectField(const FString& FieldName) const +{ + if (!JsonObj.IsValid() || !JsonObj->HasTypedField(FieldName)) + { + UE_LOG(LogSIOJ, Warning, TEXT("No field with name %s of type Object"), *FieldName); + return nullptr; + } + + TSharedPtr JsonObjField = JsonObj->GetObjectField(FieldName); + + USIOJsonObject* OutRestJsonObj = NewObject(); + OutRestJsonObj->SetRootObject(JsonObjField); + + return OutRestJsonObj; +} + +void USIOJsonObject::SetObjectField(const FString& FieldName, USIOJsonObject* JsonObject) +{ + if (!JsonObj.IsValid() || FieldName.IsEmpty()) + { + return; + } + + JsonObj->SetObjectField(FieldName, JsonObject->GetRootObject()); +} + + +TArray USIOJsonObject::GetBinaryField(const FString& FieldName) const +{ + if (!JsonObj->HasTypedField(FieldName)) + { + UE_LOG(LogSIOJ, Warning, TEXT("No field with name %s of type String"), *FieldName); + } + TSharedPtr JsonValue = JsonObj->TryGetField(FieldName); + + if (FJsonValueBinary::IsBinary(JsonValue)) + { + return FJsonValueBinary::AsBinary(JsonValue); + } + else + { + TArray EmptyArray; + return EmptyArray; + } +} + +void USIOJsonObject::SetBinaryField(const FString& FieldName, const TArray& Bytes) +{ + if (!JsonObj.IsValid() || FieldName.IsEmpty()) + { + return; + } + TSharedPtr JsonValue = MakeShareable(new FJsonValueBinary(Bytes)); + JsonObj->SetField(FieldName, JsonValue); +} + +////////////////////////////////////////////////////////////////////////// +// Array fields helpers (uniform arrays) + +TArray USIOJsonObject::GetNumberArrayField(const FString& FieldName) +{ + if (!JsonObj->HasTypedField(FieldName)) + { + UE_LOG(LogSIOJ, Warning, TEXT("No field with name %s of type Array"), *FieldName); + } + + TArray NumberArray; + if (!JsonObj.IsValid() || FieldName.IsEmpty()) + { + return NumberArray; + } + + TArray > JsonArrayValues = JsonObj->GetArrayField(FieldName); + for (TArray >::TConstIterator It(JsonArrayValues); It; ++It) + { + auto Value = (*It).Get(); + if (Value->Type != EJson::Number) + { + UE_LOG(LogSIOJ, Error, TEXT("Not Number element in array with field name %s"), *FieldName); + } + + NumberArray.Add((*It)->AsNumber()); + } + + return NumberArray; +} + +void USIOJsonObject::SetNumberArrayField(const FString& FieldName, const TArray& NumberArray) +{ + if (!JsonObj.IsValid() || FieldName.IsEmpty()) + { + return; + } + + TArray< TSharedPtr > EntriesArray; + + for (auto Number : NumberArray) + { + EntriesArray.Add(MakeShareable(new FJsonValueNumber(Number))); + } + + JsonObj->SetArrayField(FieldName, EntriesArray); +} + +TArray USIOJsonObject::GetStringArrayField(const FString& FieldName) +{ + if (!JsonObj->HasTypedField(FieldName)) + { + UE_LOG(LogSIOJ, Warning, TEXT("No field with name %s of type Array"), *FieldName); + } + + TArray StringArray; + if (!JsonObj.IsValid() || FieldName.IsEmpty()) + { + return StringArray; + } + + TArray > JsonArrayValues = JsonObj->GetArrayField(FieldName); + for (TArray >::TConstIterator It(JsonArrayValues); It; ++It) + { + auto Value = (*It).Get(); + if (Value->Type != EJson::String) + { + UE_LOG(LogSIOJ, Error, TEXT("Not String element in array with field name %s"), *FieldName); + } + + StringArray.Add((*It)->AsString()); + } + + return StringArray; +} + +void USIOJsonObject::SetStringArrayField(const FString& FieldName, const TArray& StringArray) +{ + if (!JsonObj.IsValid() || FieldName.IsEmpty()) + { + return; + } + + TArray< TSharedPtr > EntriesArray; + for (auto String : StringArray) + { + EntriesArray.Add(MakeShareable(new FJsonValueString(String))); + } + + JsonObj->SetArrayField(FieldName, EntriesArray); +} + +TArray USIOJsonObject::GetBoolArrayField(const FString& FieldName) +{ + if (!JsonObj->HasTypedField(FieldName)) + { + UE_LOG(LogSIOJ, Warning, TEXT("No field with name %s of type Array"), *FieldName); + } + + TArray BoolArray; + if (!JsonObj.IsValid() || FieldName.IsEmpty()) + { + return BoolArray; + } + + TArray > JsonArrayValues = JsonObj->GetArrayField(FieldName); + for (TArray >::TConstIterator It(JsonArrayValues); It; ++It) + { + auto Value = (*It).Get(); + if (Value->Type != EJson::Boolean) + { + UE_LOG(LogSIOJ, Error, TEXT("Not Boolean element in array with field name %s"), *FieldName); + } + + BoolArray.Add((*It)->AsBool()); + } + + return BoolArray; +} + +void USIOJsonObject::SetBoolArrayField(const FString& FieldName, const TArray& BoolArray) +{ + if (!JsonObj.IsValid() || FieldName.IsEmpty()) + { + return; + } + + TArray< TSharedPtr > EntriesArray; + for (auto Boolean : BoolArray) + { + EntriesArray.Add(MakeShareable(new FJsonValueBoolean(Boolean))); + } + + JsonObj->SetArrayField(FieldName, EntriesArray); +} + +TArray USIOJsonObject::GetObjectArrayField(const FString& FieldName) +{ + if (!JsonObj->HasTypedField(FieldName)) + { + UE_LOG(LogSIOJ, Warning, TEXT("No field with name %s of type Array"), *FieldName); + } + + TArray OutArray; + if (!JsonObj.IsValid() || FieldName.IsEmpty()) + { + return OutArray; + } + + TArray< TSharedPtr > ValArray = JsonObj->GetArrayField(FieldName); + for (auto Value : ValArray) + { + if (Value->Type != EJson::Object) + { + UE_LOG(LogSIOJ, Error, TEXT("Not Object element in array with field name %s"), *FieldName); + } + + TSharedPtr NewObj = Value->AsObject(); + + USIOJsonObject* NewJson = NewObject(); + NewJson->SetRootObject(NewObj); + + OutArray.Add(NewJson); + } + + return OutArray; +} + +void USIOJsonObject::SetObjectArrayField(const FString& FieldName, const TArray& ObjectArray) +{ + if (!JsonObj.IsValid() || FieldName.IsEmpty()) + { + return; + } + + TArray< TSharedPtr > EntriesArray; + for (auto Value : ObjectArray) + { + EntriesArray.Add(MakeShareable(new FJsonValueObject(Value->GetRootObject()))); + } + + JsonObj->SetArrayField(FieldName, EntriesArray); +} diff --git a/Source/SIOJson/Private/SIOJsonPrivatePCH.h b/Source/SIOJson/Private/SIOJsonPrivatePCH.h new file mode 100644 index 00000000..b30916be --- /dev/null +++ b/Source/SIOJson/Private/SIOJsonPrivatePCH.h @@ -0,0 +1,31 @@ +// Copyright 2014 Vladimir Alyamkin. All Rights Reserved. + +#pragma once + +#include "CoreUObject.h" +#include "Engine.h" + +#include "Delegate.h" +#include "Http.h" +#include "Map.h" +#include "Json.h" +#include "JsonUtilities.h" + +#include "LatentActions.h" +#include "Core.h" +#include "Engine.h" +#include "SharedPointer.h" + +// You should place include statements to your module's private header files here. You only need to +// add includes for headers that are used in most of your module's source files though. +#include "ModuleManager.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogSIOJ, Log, All); + +#include "ISIOJson.h" +#include "SIOJConvert.h" +#include "SIOJsonObject.h" +#include "SIOJsonValue.h" +#include "SIOJLibrary.h" +#include "SIOJRequestJSON.h" +#include "SIOJTypes.h" diff --git a/Source/SIOJson/Private/SIOJsonValue.cpp b/Source/SIOJson/Private/SIOJsonValue.cpp new file mode 100644 index 00000000..9387ace4 --- /dev/null +++ b/Source/SIOJson/Private/SIOJsonValue.cpp @@ -0,0 +1,337 @@ +// Copyright 2014 Vladimir Alyamkin. All Rights Reserved. + +#include "SIOJsonPrivatePCH.h" +#include "SIOJConvert.h" + +#pragma region FJsonValueBinary + +TArray FJsonValueBinary::AsBinary(const TSharedPtr& InJsonValue) +{ + if (FJsonValueBinary::IsBinary(InJsonValue)) + { + TSharedPtr BinaryValue = StaticCastSharedPtr(InJsonValue); + return BinaryValue->AsBinary(); + } + else + { + TArray EmptyArray; + return EmptyArray; + } +} + + +bool FJsonValueBinary::IsBinary(const TSharedPtr& InJsonValue) +{ + //use our hackery to determine if we got a binary string + bool IgnoreBool; + return !InJsonValue->TryGetBool(IgnoreBool); +} + +#pragma endregion FJsonValueBinary +#pragma region USIOJsonValue + +USIOJsonValue::USIOJsonValue(const class FObjectInitializer& PCIP) + : Super(PCIP) +{ + +} + +USIOJsonValue* USIOJsonValue::ConstructJsonValueNumber(UObject* WorldContextObject, float Number) +{ + TSharedPtr NewVal = MakeShareable(new FJsonValueNumber(Number)); + + USIOJsonValue* NewValue = NewObject(); + NewValue->SetRootValue(NewVal); + + return NewValue; +} + +USIOJsonValue* USIOJsonValue::ConstructJsonValueString(UObject* WorldContextObject, const FString& StringValue) +{ + TSharedPtr NewVal = MakeShareable(new FJsonValueString(StringValue)); + + USIOJsonValue* NewValue = NewObject(); + NewValue->SetRootValue(NewVal); + + return NewValue; +} + +USIOJsonValue* USIOJsonValue::ConstructJsonValueBool(UObject* WorldContextObject, bool InValue) +{ + TSharedPtr NewVal = MakeShareable(new FJsonValueBoolean(InValue)); + + USIOJsonValue* NewValue = NewObject(); + NewValue->SetRootValue(NewVal); + + return NewValue; +} + +USIOJsonValue* USIOJsonValue::ConstructJsonValueArray(UObject* WorldContextObject, const TArray& InArray) +{ + // Prepare data array to create new value + TArray< TSharedPtr > ValueArray; + for (auto InVal : InArray) + { + ValueArray.Add(InVal->GetRootValue()); + } + + TSharedPtr NewVal = MakeShareable(new FJsonValueArray(ValueArray)); + + USIOJsonValue* NewValue = NewObject(); + NewValue->SetRootValue(NewVal); + + return NewValue; +} + +USIOJsonValue* USIOJsonValue::ConstructJsonValueObject(USIOJsonObject *JsonObject, UObject* WorldContextObject) +{ + TSharedPtr NewVal = MakeShareable(new FJsonValueObject(JsonObject->GetRootObject())); + + USIOJsonValue* NewValue = NewObject(); + NewValue->SetRootValue(NewVal); + + return NewValue; +} + + +USIOJsonValue* USIOJsonValue::ConstructJsonValueBinary(UObject* WorldContextObject, TArray ByteArray) +{ + TSharedPtr NewVal = MakeShareable(new FJsonValueBinary(ByteArray)); + + USIOJsonValue* NewValue = NewObject(); + NewValue->SetRootValue(NewVal); + + return NewValue; +} + +USIOJsonValue* ConstructJsonValue(UObject* WorldContextObject, const TSharedPtr& InValue) +{ + TSharedPtr NewVal = InValue; + + USIOJsonValue* NewValue = NewObject(); + NewValue->SetRootValue(NewVal); + + return NewValue; +} + + +USIOJsonValue* USIOJsonValue::ValueFromJsonString(UObject* WorldContextObject, const FString& StringValue) +{ + TSharedPtr NewVal = USIOJConvert::JsonStringToJsonValue(StringValue); + + USIOJsonValue* NewValue = NewObject(); + NewValue->SetRootValue(NewVal); + + return NewValue; +} + +TSharedPtr& USIOJsonValue::GetRootValue() +{ + return JsonVal; +} + +void USIOJsonValue::SetRootValue(TSharedPtr& JsonValue) +{ + JsonVal = JsonValue; +} + + +////////////////////////////////////////////////////////////////////////// +// FJsonValue API + +ESIOJson::Type USIOJsonValue::GetType() const +{ + if (!JsonVal.IsValid()) + { + return ESIOJson::None; + } + + switch (JsonVal->Type) + { + case EJson::None: + return ESIOJson::None; + + case EJson::Null: + return ESIOJson::Null; + + case EJson::String: + if (FJsonValueBinary::IsBinary(JsonVal)) + { + return ESIOJson::Binary; + } + else + { + return ESIOJson::String; + } + case EJson::Number: + return ESIOJson::Number; + + case EJson::Boolean: + return ESIOJson::Boolean; + + case EJson::Array: + return ESIOJson::Array; + + case EJson::Object: + return ESIOJson::Object; + + default: + return ESIOJson::None; + } +} + +FString USIOJsonValue::GetTypeString() const +{ + if (!JsonVal.IsValid()) + { + return "None"; + } + + switch (JsonVal->Type) + { + case EJson::None: + return TEXT("None"); + + case EJson::Null: + return TEXT("Null"); + + case EJson::String: + return TEXT("String"); + + case EJson::Number: + return TEXT("Number"); + + case EJson::Boolean: + return TEXT("Boolean"); + + case EJson::Array: + return TEXT("Array"); + + case EJson::Object: + return TEXT("Object"); + + default: + return TEXT("None"); + } +} + +bool USIOJsonValue::IsNull() const +{ + if (!JsonVal.IsValid()) + { + return true; + } + + return JsonVal->IsNull(); +} + +float USIOJsonValue::AsNumber() const +{ + if (!JsonVal.IsValid()) + { + ErrorMessage(TEXT("Number")); + return 0.f; + } + + return JsonVal->AsNumber(); +} + +FString USIOJsonValue::AsString() const +{ + if (!JsonVal.IsValid()) + { + ErrorMessage(TEXT("String")); + return FString(); + } + + return JsonVal->AsString(); +} + +bool USIOJsonValue::AsBool() const +{ + if (!JsonVal.IsValid()) + { + ErrorMessage(TEXT("Boolean")); + return false; + } + + return JsonVal->AsBool(); +} + +TArray USIOJsonValue::AsArray() const +{ + TArray OutArray; + + if (!JsonVal.IsValid()) + { + ErrorMessage(TEXT("Array")); + return OutArray; + } + + TArray< TSharedPtr > ValArray = JsonVal->AsArray(); + for (auto Value : ValArray) + { + USIOJsonValue* NewValue = NewObject(); + NewValue->SetRootValue(Value); + + OutArray.Add(NewValue); + } + + return OutArray; +} + +USIOJsonObject* USIOJsonValue::AsObject() +{ + if (!JsonVal.IsValid()) + { + ErrorMessage(TEXT("Object")); + return nullptr; + } + + TSharedPtr NewObj = JsonVal->AsObject(); + + USIOJsonObject* JsonObj = NewObject(); + JsonObj->SetRootObject(NewObj); + + return JsonObj; +} + + +TArray USIOJsonValue::AsBinary() +{ + if (!JsonVal.IsValid()) + { + ErrorMessage(TEXT("Binary")); + TArray ByteArray; + return ByteArray; + } + + //binary object pretending & starts with non-json format? it's our disguise binary + if (JsonVal->Type == EJson::String && + FJsonValueBinary::IsBinary(JsonVal)) + { + //Valid binary available + return FJsonValueBinary::AsBinary(JsonVal); + } + else + { + //Empty array + TArray ByteArray; + return ByteArray; + } +} + +FString USIOJsonValue::EncodeJson() const +{ + return USIOJConvert::ToJsonString(JsonVal); +} + +////////////////////////////////////////////////////////////////////////// +// Helpers + +void USIOJsonValue::ErrorMessage(const FString& InType) const +{ + UE_LOG(LogSIOJ, Error, TEXT("Json Value of type '%s' used as a '%s'."), *GetTypeString(), *InType); +} + +#pragma endregion USIOJsonValue \ No newline at end of file diff --git a/Source/SIOJson/Public/ISIOJson.h b/Source/SIOJson/Public/ISIOJson.h new file mode 100644 index 00000000..62c5bf4f --- /dev/null +++ b/Source/SIOJson/Public/ISIOJson.h @@ -0,0 +1,38 @@ +// Copyright 2014 Vladimir Alyamkin. All Rights Reserved. + +#pragma once + +#include "ModuleManager.h" + + +/** + * The public interface to this module. In most cases, this interface is only public to sibling modules + * within this plugin. + */ +class ISIOJson : public IModuleInterface +{ + +public: + + /** + * Singleton-like access to this module's interface. This is just for convenience! + * Beware of calling this during the shutdown phase, though. Your module might have been unloaded already. + * + * @return Returns singleton instance, loading the module on demand if needed + */ + static inline ISIOJson& Get() + { + return FModuleManager::LoadModuleChecked< ISIOJson >( "SIOJson" ); + } + + /** + * Checks to see if this module is loaded and ready. It is only valid to call Get() if IsAvailable() returns true. + * + * @return True if the module is loaded and ready to use + */ + static inline bool IsAvailable() + { + return FModuleManager::Get().IsModuleLoaded( "SIOJson" ); + } +}; + diff --git a/Source/SIOJson/Public/SIOJConvert.h b/Source/SIOJson/Public/SIOJConvert.h new file mode 100644 index 00000000..163cf380 --- /dev/null +++ b/Source/SIOJson/Public/SIOJConvert.h @@ -0,0 +1,62 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "Object.h" +#include "SIOJConvert.generated.h" + +struct FTrimmedKeyMap +{ + FString LongKey; + TMap> SubMap; +}; + +/** + * + */ +UCLASS() +class SIOJSON_API USIOJConvert : public UObject +{ + GENERATED_BODY() +public: + + //encode/decode json convenience wrappers + static FString ToJsonString(const TSharedPtr& JsonObject); + static FString ToJsonString(const TSharedPtr& JsonValue); + static FString ToJsonString(const TArray>& JsonValueArray); + + static TSharedPtr ToJsonObject(const FString& JsonString); + static TSharedPtr MakeJsonObject(); + + + //Structs + + //Will trim names if specified as blueprint + static TSharedPtr ToJsonObject(UStruct* Struct, void* StructPtr, bool IsBlueprintStruct = false); + + //Expects a JsonObject, if blueprint struct it will lengthen the names to fill properly + static bool JsonObjectToUStruct(TSharedPtr JsonObject, UStruct* Struct, void* StructPtr, bool IsBlueprintStruct = false); + + + //typically from callbacks + static class USIOJsonValue* ToSIOJsonValue(const TArray>& JsonValueArray); + + //Convenience overrides for JsonValues + static TSharedPtr ToJsonValue(const TSharedPtr& JsonObject); + static TSharedPtr ToJsonValue(const FString& StringValue); + static TSharedPtr ToJsonValue(double NumberValue); + static TSharedPtr ToJsonValue(bool BoolValue); + static TSharedPtr ToJsonValue(const TArray& BinaryValue); + static TSharedPtr ToJsonValue(const TArray>& ArrayValue); + + static TSharedPtr JsonStringToJsonValue(const FString& JsonString); + static TArray> JsonStringToJsonArray(const FString& JsonString); + + + //internal utility, exposed for modularity + static void TrimValueKeyNames(const TSharedPtr& JsonValue); + static bool TrimKey(const FString& InLongKey, FString& OutTrimmedKey); + static void SetTrimmedKeyMapForStruct(TSharedPtr& InMap, UStruct* Struct); + static void SetTrimmedKeyMapForProp(TSharedPtr& InMap, UProperty* ArrayInnerProp); + static void ReplaceJsonValueNamesWithMap(TSharedPtr& InValue, TSharedPtr KeyMap); +}; diff --git a/Source/SIOJson/Public/SIOJLibrary.h b/Source/SIOJson/Public/SIOJLibrary.h new file mode 100644 index 00000000..61543a2b --- /dev/null +++ b/Source/SIOJson/Public/SIOJLibrary.h @@ -0,0 +1,199 @@ +// Copyright 2016 Vladimir Alyamkin. All Rights Reserved. + +#pragma once + +#include "Kismet/BlueprintFunctionLibrary.h" + +#include "SIOJTypes.h" +#include "SIOJLibrary.generated.h" + +class USIOJRequestJSON; +class USIOJsonObject; + +DECLARE_DYNAMIC_DELEGATE_OneParam(FSIOJCallDelegate, USIOJRequestJSON*, Request); + +USTRUCT() +struct FSIOJCallResponse +{ + GENERATED_USTRUCT_BODY() + + UPROPERTY() + USIOJRequestJSON* Request; + + UPROPERTY() + UObject* WorldContextObject; + + UPROPERTY() + FSIOJCallDelegate Callback; + + FDelegateHandle CompleteDelegateHandle; + FDelegateHandle FailDelegateHandle; + + FSIOJCallResponse() + : Request(nullptr) + , WorldContextObject(nullptr) + { + } + +}; + +/** + * Usefull tools for REST communications + */ +UCLASS() +class USIOJLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + + + ////////////////////////////////////////////////////////////////////////// + // Helpers + +public: + /** Applies percent-encoding to text */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Utility") + static FString PercentEncode(const FString& Source); + + /** + * Encodes a FString into a Base64 string + * + * @param Source The string data to convert + * @return A string that encodes the binary data in a way that can be safely transmitted via various Internet protocols + */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Utility", meta = (DisplayName = "Base64 Encode")) + static FString Base64Encode(const FString& Source); + + /** + * Decodes a Base64 string into a FString + * + * @param Source The stringified data to convert + * @param Dest The out buffer that will be filled with the decoded data + * @return True if the buffer was decoded, false if it failed to decode + */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Utility", meta = (DisplayName = "Base64 Decode")) + static bool Base64Decode(const FString& Source, FString& Dest); + + ////////////////////////////////////////////////////////////////////////// + // Easy URL processing + + /** + * Decodes a json string into an array of stringified json Values + * + * @param JsonString Input stringified json + * @param OutJsonValueArray The decoded Array of JsonValue + */ + UFUNCTION(BlueprintPure, Category = "SIOJ|Utility") + static bool StringToJsonValueArray(const FString& JsonString, TArray& OutJsonValueArray); + + /** + * Uses the reflection system to convert an unreal struct into a JsonObject + * + * @param AnyStruct The struct you wish to convert + * @return Converted Json Object + */ + UFUNCTION(BlueprintPure, Category = "SocketIOFunctions", CustomThunk, meta = (CustomStructureParam = "AnyStruct")) + static USIOJsonObject* StructToJsonObject(UProperty* AnyStruct); + + /** + * Uses the reflection system to fill an unreal struct from data defined in JsonObject. + * + * @param JsonObject The source JsonObject for properties to fill + * @param AnyStruct The struct you wish to fill + * @return Whether all properties filled correctly + */ + UFUNCTION(BlueprintCallable, Category = "SocketIOFunctions", CustomThunk, meta = (CustomStructureParam = "AnyStruct")) + static bool JsonObjectToStruct(USIOJsonObject* JsonObject, UProperty* AnyStruct); + + //Convert property into c++ accessible form + DECLARE_FUNCTION(execStructToJsonObject) + { + //Get properties and pointers from stack + Stack.Step(Stack.Object, NULL); + UStructProperty* StructProperty = ExactCast(Stack.MostRecentProperty); + void* StructPtr = Stack.MostRecentPropertyAddress; + + // We need this to wrap up the stack + P_FINISH; + + auto BPJsonObject = NewObject(); + + auto JsonObject = USIOJConvert::ToJsonObject(StructProperty->Struct, StructPtr, true); + BPJsonObject->SetRootObject(JsonObject); + + *(USIOJsonObject**)RESULT_PARAM = BPJsonObject; + } + + DECLARE_FUNCTION(execJsonObjectToStruct) + { + //Get properties and pointers from stack + P_GET_OBJECT(USIOJsonObject, JsonObject); + + Stack.Step(Stack.Object, NULL); + UStructProperty* StructProperty = ExactCast(Stack.MostRecentProperty); + void* StructPtr = Stack.MostRecentPropertyAddress; + + P_FINISH; + + //Pass in the reference to the json object + bool Success = USIOJConvert::JsonObjectToUStruct(JsonObject->GetRootObject(), StructProperty->Struct, StructPtr, true); + + *(bool*)RESULT_PARAM = Success; + } + + //Conversion Nodes + + //ToJsonValue + + UFUNCTION(BlueprintPure, meta = (DisplayName = "ToJsonValue (Array)", BlueprintAutocast), Category = "Utilities|SocketIO") + static USIOJsonValue* Conv_ArrayToJsonValue(const TArray& InArray); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "ToJsonValue (JsonObject)", BlueprintAutocast), Category = "Utilities|SocketIO") + static USIOJsonValue* Conv_JsonObjectToJsonValue(USIOJsonObject* InObject); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "ToJsonValue (Bytes)", BlueprintAutocast), Category = "Utilities|SocketIO") + static USIOJsonValue* Conv_BytesToJsonValue(const TArray& InBytes); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "ToJsonValue (String)", BlueprintAutocast), Category = "Utilities|SocketIO") + static USIOJsonValue* Conv_StringToJsonValue(const FString& InString); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "ToJsonValue (Integer)", BlueprintAutocast), Category = "Utilities|SocketIO") + static USIOJsonValue* Conv_IntToJsonValue(int32 InInt); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "ToJsonValue (Float)", BlueprintAutocast), Category = "Utilities|SocketIO") + static USIOJsonValue* Conv_FloatToJsonValue(float InFloat); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "ToJsonValue (Bool)", BlueprintAutocast), Category = "Utilities|SocketIO") + static USIOJsonValue* Conv_BoolToJsonValue(bool InBool); + + //To Native Types + UFUNCTION(BlueprintPure, meta = (DisplayName = "To Integer (JsonValue)", BlueprintAutocast), Category = "Utilities|SocketIO") + static int32 Conv_JsonValueToInt(class USIOJsonValue* InValue); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "To Float (JsonValue)", BlueprintAutocast), Category = "Utilities|SocketIO") + static float Conv_JsonValueToFloat(class USIOJsonValue* InValue); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "To Bool (JsonValue)", BlueprintAutocast), Category = "Utilities|SocketIO") + static bool Conv_JsonValueToBool(class USIOJsonValue* InValue); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "To Bytes (JsonValue)", BlueprintAutocast), Category = "Utilities|SocketIO") + static TArray Conv_JsonValueToBytes(class USIOJsonValue* InValue); + + //ToString - these never get called sadly, kismet library get display name takes priority + UFUNCTION(BlueprintPure, meta = (DisplayName = "ToString (SIOJsonObject)", CompactNodeTitle = "->", BlueprintAutocast), Category = "Utilities|SocketIO") + static FString Conv_JsonObjectToString(class USIOJsonObject* InObject); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "To Object (JsonValue)", BlueprintAutocast), Category = "Utilities|SocketIO") + static USIOJsonObject* Conv_JsonValueToJsonObject(class USIOJsonValue* InValue); + +public: + /** Easy way to process http requests */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Utility", meta = (WorldContext = "WorldContextObject")) + static void CallURL(UObject* WorldContextObject, const FString& URL, ERequestVerb Verb, ERequestContentType ContentType, USIOJsonObject* SIOJJson, const FSIOJCallDelegate& Callback); + + /** Called when URL is processed (one for both success/unsuccess events)*/ + static void OnCallComplete(USIOJRequestJSON* Request); + +private: + static TMap RequestMap; + +}; diff --git a/Source/SIOJson/Public/SIOJRequestJSON.h b/Source/SIOJson/Public/SIOJRequestJSON.h new file mode 100644 index 00000000..090d9094 --- /dev/null +++ b/Source/SIOJson/Public/SIOJRequestJSON.h @@ -0,0 +1,301 @@ +// Copyright 2014 Vladimir Alyamkin. All Rights Reserved. + +#pragma once + +#include "Delegate.h" +#include "Http.h" +#include "Map.h" +#include "Json.h" + +#include "SIOJTypes.h" +#include "SIOJRequestJSON.generated.h" + +/** + * @author Original latent action class by https://github.com/unktomi + */ +template class FSIOJLatentAction : public FPendingLatentAction +{ +public: + virtual void Call(const T &Value) + { + Result = Value; + Called = true; + } + + void operator()(const T &Value) + { + Call(Value); + } + + void Cancel(); + + FSIOJLatentAction(FWeakObjectPtr RequestObj, T& ResultParam, const FLatentActionInfo& LatentInfo) : + Called(false), + Request(RequestObj), + ExecutionFunction(LatentInfo.ExecutionFunction), + OutputLink(LatentInfo.Linkage), + CallbackTarget(LatentInfo.CallbackTarget), + Result(ResultParam) + { + } + + virtual void UpdateOperation(FLatentResponse& Response) override + { + Response.FinishAndTriggerIf(Called, ExecutionFunction, OutputLink, CallbackTarget); + } + + virtual void NotifyObjectDestroyed() + { + Cancel(); + } + + virtual void NotifyActionAborted() + { + Cancel(); + } + +private: + bool Called; + FWeakObjectPtr Request; + +public: + const FName ExecutionFunction; + const int32 OutputLink; + const FWeakObjectPtr CallbackTarget; + T &Result; +}; + + +/** Generate a delegates for callback events */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnRequestComplete, class USIOJRequestJSON*, Request); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnRequestFail, class USIOJRequestJSON*, Request); + +DECLARE_MULTICAST_DELEGATE_OneParam(FOnStaticRequestComplete, class USIOJRequestJSON*); +DECLARE_MULTICAST_DELEGATE_OneParam(FOnStaticRequestFail, class USIOJRequestJSON*); + + +/** + * General helper class http requests via blueprints + */ +UCLASS(BlueprintType, Blueprintable) +class SIOJSON_API USIOJRequestJSON : public UObject +{ + GENERATED_UCLASS_BODY() + +public: + ////////////////////////////////////////////////////////////////////////// + // Construction + + /** Creates new request (totally empty) */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Construct Json Request (Empty)", HidePin = "WorldContextObject", DefaultToSelf = "WorldContextObject"), Category = "SIOJ|Request") + static USIOJRequestJSON* ConstructRequest(UObject* WorldContextObject); + + /** Creates new request with defined verb and content type */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Construct Json Request", HidePin = "WorldContextObject", DefaultToSelf = "WorldContextObject"), Category = "SIOJ|Request") + static USIOJRequestJSON* ConstructRequestExt(UObject* WorldContextObject, ERequestVerb Verb, ERequestContentType ContentType); + + /** Set verb to the request */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Request") + void SetVerb(ERequestVerb Verb); + + /** Set custom verb to the request */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Request") + void SetCustomVerb(FString Verb); + + /** Set content type to the request. If you're using the x-www-form-urlencoded, + * params/constaints should be defined as key=ValueString pairs from Json data */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Request") + void SetContentType(ERequestContentType ContentType); + + /** Set content type of the request for binary post data */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Request") + void SetBinaryContentType(const FString &ContentType); + + /** Set content of the request for binary post data */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Request") + void SetBinaryRequestContent(const TArray &Content); + + /** Sets optional header info */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Request") + void SetHeader(const FString& HeaderName, const FString& HeaderValue); + + + ////////////////////////////////////////////////////////////////////////// + // Destruction and reset + + /** Reset all internal saved data */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Utility") + void ResetData(); + + /** Reset saved request data */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Request") + void ResetRequestData(); + + /** Reset saved response data */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Response") + void ResetResponseData(); + + /** Cancel latent response waiting */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Response") + void Cancel(); + + + ////////////////////////////////////////////////////////////////////////// + // JSON data accessors + + /** Get the Request Json object */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Request") + USIOJsonObject* GetRequestObject(); + + /** Set the Request Json object */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Request") + void SetRequestObject(USIOJsonObject* JsonObject); + + /** Get the Response Json object */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Response") + USIOJsonObject* GetResponseObject(); + + /** Set the Response Json object */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Response") + void SetResponseObject(USIOJsonObject* JsonObject); + + + /////////////////////////////////////////////////////////////////////////// + // Request/response data access + + /** Get url of http request */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Request") + FString GetURL(); + + /** Get status of http request */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Request") + ERequestStatus GetStatus(); + + /** Get the response code of the last query */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Response") + int32 GetResponseCode(); + + /** Get value of desired response header */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Response") + FString GetResponseHeader(const FString HeaderName); + + /** Get list of all response headers */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Response") + TArray GetAllResponseHeaders(); + + + ////////////////////////////////////////////////////////////////////////// + // URL processing + + /** Open URL with current setup */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Request") + virtual void ProcessURL(const FString& Url = TEXT("http://alyamkin.com")); + + /** Open URL in latent mode */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Request", meta = (Latent, LatentInfo = "LatentInfo", HidePin = "WorldContextObject", DefaultToSelf = "WorldContextObject")) + virtual void ApplyURL(const FString& Url, USIOJsonObject *&Result, UObject* WorldContextObject, struct FLatentActionInfo LatentInfo); + + /** Apply current internal setup to request and process it */ + void ProcessRequest(); + + + ////////////////////////////////////////////////////////////////////////// + // Request callbacks + +private: + /** Internal bind function for the IHTTPRequest::OnProcessRequestCompleted() event */ + void OnProcessRequestComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful); + +public: + /** Event occured when the request has been completed */ + UPROPERTY(BlueprintAssignable, Category = "SIOJ|Event") + FOnRequestComplete OnRequestComplete; + + /** Event occured when the request wasn't successfull */ + UPROPERTY(BlueprintAssignable, Category = "SIOJ|Event") + FOnRequestFail OnRequestFail; + + /** Event occured when the request has been completed */ + FOnStaticRequestComplete OnStaticRequestComplete; + + /** Event occured when the request wasn't successfull */ + FOnStaticRequestFail OnStaticRequestFail; + + + ////////////////////////////////////////////////////////////////////////// + // Tags + +public: + /** Add tag to this request */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Utility") + void AddTag(FName Tag); + + /** + * Remove tag from this request + * + * @return Number of removed elements + */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Utility") + int32 RemoveTag(FName Tag); + + /** See if this request contains the supplied tag */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Utility") + bool HasTag(FName Tag) const; + +protected: + /** Array of tags that can be used for grouping and categorizing */ + TArray Tags; + + + ////////////////////////////////////////////////////////////////////////// + // Data + +public: + /** Request response stored as a string */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "SIOJ|Response") + FString ResponseContent; + + /** Is the response valid JSON? */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "SIOJ|Response") + bool bIsValidJsonResponse; + +protected: + /** Latent action helper */ + FSIOJLatentAction* ContinueAction; + + /** Internal request data stored as JSON */ + UPROPERTY() + USIOJsonObject* RequestJsonObj; + + UPROPERTY() + TArray RequestBytes; + + UPROPERTY() + FString BinaryContentType; + + /** Response data stored as JSON */ + UPROPERTY() + USIOJsonObject* ResponseJsonObj; + + /** Verb for making request (GET,POST,etc) */ + ERequestVerb RequestVerb; + + /** Content type to be applied for request */ + ERequestContentType RequestContentType; + + /** Mapping of header section to values. Used to generate final header string for request */ + TMap RequestHeaders; + + /** Cached key/value header pairs. Parsed once request completes */ + TMap ResponseHeaders; + + /** Http Response code */ + int32 ResponseCode; + + /** Custom verb that will be used with RequestContentType == CUSTOM */ + FString CustomVerb; + + /** Request we're currently processing */ + TSharedRef HttpRequest = FHttpModule::Get().CreateRequest(); + +}; diff --git a/Source/SIOJson/Public/SIOJTypes.h b/Source/SIOJson/Public/SIOJTypes.h new file mode 100644 index 00000000..0ba037e2 --- /dev/null +++ b/Source/SIOJson/Public/SIOJTypes.h @@ -0,0 +1,41 @@ +// Copyright 2016 Vladimir Alyamkin. All Rights Reserved. + +#pragma once + +/** Verb (GET, PUT, POST) used by the request */ +UENUM(BlueprintType) +enum class ERequestVerb : uint8 +{ + GET, + POST, + PUT, + DEL UMETA(DisplayName = "DELETE"), + /** Set CUSTOM verb by SetCustomVerb() function */ + CUSTOM +}; + +/** Content type (json, urlencoded, etc.) used by the request */ +UENUM(BlueprintType) +enum class ERequestContentType : uint8 +{ + x_www_form_urlencoded_url UMETA(DisplayName = "x-www-form-urlencoded (URL)"), + x_www_form_urlencoded_body UMETA(DisplayName = "x-www-form-urlencoded (Request Body)"), + json, + binary +}; + +/** Enumerates the current state of an Http request */ +UENUM(BlueprintType) +enum class ERequestStatus : uint8 +{ + /** Has not been started via ProcessRequest() */ + NotStarted, + /** Currently being ticked and processed */ + Processing, + /** Finished but failed */ + Failed, + /** Failed because it was unable to connect (safe to retry) */ + Failed_ConnectionError, + /** Finished and was successful */ + Succeeded +}; diff --git a/Source/SIOJson/Public/SIOJsonObject.h b/Source/SIOJson/Public/SIOJsonObject.h new file mode 100644 index 00000000..2f76f0a7 --- /dev/null +++ b/Source/SIOJson/Public/SIOJsonObject.h @@ -0,0 +1,176 @@ +// Copyright 1998-2014 Epic Games, Inc. All Rights Reserved. +// Copyright 2014 Vladimir Alyamkin. All Rights Reserved. + +#pragma once + +#include "SIOJsonObject.generated.h" + +class USIOJsonValue; + +/** + * Blueprintable FJsonObject wrapper + */ +UCLASS(BlueprintType, Blueprintable) +class SIOJSON_API USIOJsonObject : public UObject +{ + GENERATED_UCLASS_BODY() + + /** Create new Json object, cannot be pure */ + UFUNCTION(BlueprintCallable , meta = (DisplayName = "Construct Json Object", HidePin = "WorldContextObject", DefaultToSelf = "WorldContextObject"), Category = "SIOJ|Json") + static USIOJsonObject* ConstructJsonObject(UObject* WorldContextObject); + + /** Reset all internal data */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + void Reset(); + + /** Get the root Json object */ + TSharedPtr& GetRootObject(); + + /** Set the root Json object */ + void SetRootObject(TSharedPtr& JsonObject); + + + ////////////////////////////////////////////////////////////////////////// + // Serialization + + /** Serialize Json to string (formatted with line breaks) */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + FString EncodeJson() const; + + /** Serialize Json to string (single string without line breaks) */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + FString EncodeJsonToSingleString() const; + + /** Construct Json object from string */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + bool DecodeJson(const FString& JsonString); + + + ////////////////////////////////////////////////////////////////////////// + // FJsonObject API + + /** Returns a list of field names that exist in the object */ + UFUNCTION(BlueprintPure, Category = "SIOJ|Json") + TArray GetFieldNames(); + + /** Checks to see if the FieldName exists in the object */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + bool HasField(const FString& FieldName) const; + + /** Remove field named FieldName */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + void RemoveField(const FString& FieldName); + + /** Get the field named FieldName as a JsonValue */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + USIOJsonValue* GetField(const FString& FieldName) const; + + /** Add a field named FieldName with a Value */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + void SetField(const FString& FieldName, USIOJsonValue* JsonValue); + + /** Get the field named FieldName as a Json Array */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + TArray GetArrayField(const FString& FieldName); + + /** Set an ObjectField named FieldName and value of Json Array */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + void SetArrayField(const FString& FieldName, const TArray& InArray); + + /** Adds all of the fields from one json object to this one */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + void MergeJsonObject(USIOJsonObject* InJsonObject, bool Overwrite); + + + ////////////////////////////////////////////////////////////////////////// + // FJsonObject API Helpers (easy to use with simple Json objects) + + /** Get the field named FieldName as a number. Ensures that the field is present and is of type Json number. + * Attn.!! float used instead of double to make the function blueprintable! */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + float GetNumberField(const FString& FieldName) const; + + /** Add a field named FieldName with Number as value + * Attn.!! float used instead of double to make the function blueprintable! */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + void SetNumberField(const FString& FieldName, float Number); + + /** Get the field named FieldName as a string. */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + FString GetStringField(const FString& FieldName) const; + + /** Add a field named FieldName with value of StringValue */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + void SetStringField(const FString& FieldName, const FString& StringValue); + + /** Get the field named FieldName as a boolean. */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + bool GetBoolField(const FString& FieldName) const; + + /** Set a boolean field named FieldName and value of InValue */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + void SetBoolField(const FString& FieldName, bool InValue); + + /** Get the field named FieldName as a Json object. */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + USIOJsonObject* GetObjectField(const FString& FieldName) const; + + /** Set an ObjectField named FieldName and value of JsonObject */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + void SetObjectField(const FString& FieldName, USIOJsonObject* JsonObject); + + /** Get the field named FieldName as a binary buffer array. */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + TArray GetBinaryField(const FString& FieldName) const; + + /** Set an BinaryField named FieldName and binary buffer array */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + void SetBinaryField(const FString& FieldName, const TArray& Bytes); + + + ////////////////////////////////////////////////////////////////////////// + // Array fields helpers (uniform arrays) + + /** Get the field named FieldName as a Number Array. Use it only if you're sure that array is uniform! + * Attn.!! float used instead of double to make the function blueprintable! */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + TArray GetNumberArrayField(const FString& FieldName); + + /** Set an ObjectField named FieldName and value of Number Array + * Attn.!! float used instead of double to make the function blueprintable! */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + void SetNumberArrayField(const FString& FieldName, const TArray& NumberArray); + + /** Get the field named FieldName as a String Array. Use it only if you're sure that array is uniform! */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + TArray GetStringArrayField(const FString& FieldName); + + /** Set an ObjectField named FieldName and value of String Array */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + void SetStringArrayField(const FString& FieldName, const TArray& StringArray); + + /** Get the field named FieldName as a Bool Array. Use it only if you're sure that array is uniform! */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + TArray GetBoolArrayField(const FString& FieldName); + + /** Set an ObjectField named FieldName and value of Bool Array */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + void SetBoolArrayField(const FString& FieldName, const TArray& BoolArray); + + /** Get the field named FieldName as an Object Array. Use it only if you're sure that array is uniform! */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + TArray GetObjectArrayField(const FString& FieldName); + + /** Set an ObjectField named FieldName and value of Ob Array */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + void SetObjectArrayField(const FString& FieldName, const TArray& ObjectArray); + + + ////////////////////////////////////////////////////////////////////////// + // Data + +private: + /** Internal JSON data */ + TSharedPtr JsonObj; + +}; diff --git a/Source/SIOJson/Public/SIOJsonValue.h b/Source/SIOJson/Public/SIOJsonValue.h new file mode 100644 index 00000000..fc086b33 --- /dev/null +++ b/Source/SIOJson/Public/SIOJsonValue.h @@ -0,0 +1,168 @@ +// Copyright 1998-2014 Epic Games, Inc. All Rights Reserved. +// Copyright 2014 Vladimir Alyamkin. All Rights Reserved. + +#pragma once + +#include "SIOJsonValue.generated.h" + +class USIOJsonObject; + +/** + * Represents all the types a Json Value can be. + */ +UENUM(BlueprintType) +namespace ESIOJson +{ + enum Type + { + None, + Null, + String, + Number, + Boolean, + Array, + Object, + Binary + }; +} + +class SIOJSON_API FJsonValueBinary : public FJsonValue +{ +public: + FJsonValueBinary(const TArray& InBinary) : Value(InBinary) { Type = EJson::String; } //pretends to be none + + virtual bool TryGetString(FString& OutString) const override + { + OutString = FString::FromHexBlob(Value.GetData(), Value.Num()); //encode the binary into the string directly + return true; + } + virtual bool TryGetNumber(double& OutDouble) const override + { + OutDouble = Value.Num(); + return true; + } + + //hackery: we use this as an indicator we have a binary (strings don't normally do this) + virtual bool TryGetBool(bool& OutBool) const override { return false; } + + /** Return our binary data from this value */ + TArray AsBinary() { return Value; } + + /** Convenience method to determine if passed FJsonValue is a FJsonValueBinary or not. */ + static bool IsBinary(const TSharedPtr& InJsonValue); + + /** Convenience method to get binary array from unknown JsonValue, test with IsBinary first. */ + static TArray AsBinary(const TSharedPtr& InJsonValue); + +protected: + TArray Value; + + virtual FString GetType() const override { return TEXT("Binary"); } +}; + +/** + * Blueprintable FJsonValue wrapper + */ +UCLASS(BlueprintType, Blueprintable) +class SIOJSON_API USIOJsonValue : public UObject +{ + GENERATED_UCLASS_BODY() + + /** Create new Json Number value + * Attn.!! float used instead of double to make the function blueprintable! */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Construct Json Number Value", HidePin = "WorldContextObject", DefaultToSelf = "WorldContextObject"), Category = "SIOJ|Json") + static USIOJsonValue* ConstructJsonValueNumber(UObject* WorldContextObject, float Number); + + /** Create new Json String value */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Construct Json String Value", HidePin = "WorldContextObject", DefaultToSelf = "WorldContextObject"), Category = "SIOJ|Json") + static USIOJsonValue* ConstructJsonValueString(UObject* WorldContextObject, const FString& StringValue); + + /** Create new Json Bool value */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Construct Json Bool Value", HidePin = "WorldContextObject", DefaultToSelf = "WorldContextObject"), Category = "SIOJ|Json") + static USIOJsonValue* ConstructJsonValueBool(UObject* WorldContextObject, bool InValue); + + /** Create new Json Array value */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Construct Json Array Value", HidePin = "WorldContextObject", DefaultToSelf = "WorldContextObject"), Category = "SIOJ|Json") + static USIOJsonValue* ConstructJsonValueArray(UObject* WorldContextObject, const TArray& InArray); + + /** Create new Json Object value */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Construct Json Object Value", HidePin = "WorldContextObject", DefaultToSelf = "WorldContextObject"), Category = "SIOJ|Json") + static USIOJsonValue* ConstructJsonValueObject(USIOJsonObject *JsonObject, UObject* WorldContextObject); + + /** Create new Json Binary value */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Construct Json Binary Value", HidePin = "WorldContextObject", DefaultToSelf = "WorldContextObject"), Category = "SIOJ|Json") + static USIOJsonValue* ConstructJsonValueBinary(UObject* WorldContextObject, TArray ByteArray); + + /** Create new Json value from FJsonValue (to be used from USIOJsonObject) */ + static USIOJsonValue* ConstructJsonValue(UObject* WorldContextObject, const TSharedPtr& InValue); + + /** Create new Json value from JSON encoded string*/ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Value From Json String", HidePin = "WorldContextObject", DefaultToSelf = "WorldContextObject"), Category = "SIOJ|Json") + static USIOJsonValue* ValueFromJsonString(UObject* WorldContextObject, const FString& StringValue); + + /** Get the root Json value */ + TSharedPtr& GetRootValue(); + + /** Set the root Json value */ + void SetRootValue(TSharedPtr& JsonValue); + + + ////////////////////////////////////////////////////////////////////////// + // FJsonValue API + + /** Get type of Json value (Enum) */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + ESIOJson::Type GetType() const; + + /** Get type of Json value (String) */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + FString GetTypeString() const; + + /** Returns true if this value is a 'null' */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + bool IsNull() const; + + /** Returns this value as a double, throwing an error if this is not an Json Number + * Attn.!! float used instead of double to make the function blueprintable! */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + float AsNumber() const; + + /** Returns this value as a number, throwing an error if this is not an Json String */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + FString AsString() const; + + /** Returns this value as a boolean, throwing an error if this is not an Json Bool */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + bool AsBool() const; + + /** Returns this value as an array, throwing an error if this is not an Json Array */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + TArray AsArray() const; + + /** Returns this value as an object, throwing an error if this is not an Json Object */ + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + USIOJsonObject* AsObject(); + + //todo: add basic binary e.g. tarray + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + TArray AsBinary(); + + UFUNCTION(BlueprintCallable, Category = "SIOJ|Json") + FString EncodeJson() const; + + ////////////////////////////////////////////////////////////////////////// + // Data + +private: + /** Internal JSON data */ + TSharedPtr JsonVal; + + + ////////////////////////////////////////////////////////////////////////// + // Helpers + +protected: + /** Simple error logger */ + void ErrorMessage(const FString& InType) const; + +}; diff --git a/Source/SIOJson/SIOJson.Build.cs b/Source/SIOJson/SIOJson.Build.cs new file mode 100644 index 00000000..3e56a6b2 --- /dev/null +++ b/Source/SIOJson/SIOJson.Build.cs @@ -0,0 +1,30 @@ +// Copyright 2014 Vladimir Alyamkin. All Rights Reserved. + +using System.IO; + +namespace UnrealBuildTool.Rules +{ + public class SIOJson : ModuleRules + { + public SIOJson(TargetInfo Target) + { + PrivateIncludePaths.AddRange( + new string[] { + "SIOJson/Private", + // ... add other private include paths required here ... + }); + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "CoreUObject", + "Engine", + "HTTP", + "Json", + "JsonUtilities" + // ... add other public dependencies that you statically link with here ... + }); + } + } +} \ No newline at end of file diff --git a/Source/SocketIOClient/Private/SIOMessageConvert.cpp b/Source/SocketIOClient/Private/SIOMessageConvert.cpp new file mode 100644 index 00000000..2302a2e2 --- /dev/null +++ b/Source/SocketIOClient/Private/SIOMessageConvert.cpp @@ -0,0 +1,152 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#include "SocketIOClientPrivatePCH.h" + +DEFINE_LOG_CATEGORY(SocketIOLog); + +typedef TJsonWriterFactory< TCHAR, TCondensedJsonPrintPolicy > FCondensedJsonStringWriterFactory; +typedef TJsonWriter< TCHAR, TCondensedJsonPrintPolicy > FCondensedJsonStringWriter; + +TSharedPtr USIOMessageConvert::ToJsonValue(const sio::message::ptr& Message) +{ + auto flag = Message->get_flag(); + + if (flag == sio::message::flag_integer) + { + return MakeShareable(new FJsonValueNumber(Message->get_int())); + } + else if (flag == sio::message::flag_double) + { + return MakeShareable(new FJsonValueNumber(Message->get_double())); + } + else if (flag == sio::message::flag_string) + { + return MakeShareable(new FJsonValueString(FStringFromStd(Message->get_string()))); + } + else if (flag == sio::message::flag_binary) + { + //FString WarningString = FString::Printf(TEXT(""), Binary->length()); + + //convert sio buffer ptr into the array + TArray Buffer; + Buffer.Append((uint8*)(Message->get_binary()->data()), Message->get_binary()->size()); + //todo: investigate if binary optimization is possible? Do we copy? + + return MakeShareable(new FJsonValueBinary(Buffer)); + } + else if (flag == sio::message::flag_array) + { + auto MessageVector = Message->get_vector(); + TArray< TSharedPtr > InArray; + + InArray.Reset(MessageVector.size()); + + for (auto ItemMessage : MessageVector) + { + InArray.Add(ToJsonValue(ItemMessage)); + } + + return MakeShareable(new FJsonValueArray(InArray)); + } + else if (flag == sio::message::flag_object) + { + auto MessageMap = Message->get_map(); + TSharedPtr InObject = MakeShareable(new FJsonObject()); + + for (auto MapPair : MessageMap) + { + InObject->SetField(FStringFromStd(MapPair.first), ToJsonValue(MapPair.second)); + } + + return MakeShareable(new FJsonValueObject(InObject)); + } + else if (flag == sio::message::flag_boolean) + { + bool InBoolean = false; + + return MakeShareable(new FJsonValueBoolean(InBoolean)); + } + else if (flag == sio::message::flag_null) + { + return MakeShareable(new FJsonValueNull()); + } + else + { + return MakeShareable(new FJsonValueNull()); + } +} + + + +sio::message::ptr USIOMessageConvert::ToSIOMessage(const TSharedPtr& JsonValue) +{ + if (JsonValue->Type == EJson::None) + { + return sio::null_message::create(); + } + else if (JsonValue->Type == EJson::Null) + { + return sio::null_message::create(); + } + else if (JsonValue->Type == EJson::String) + { + if (FJsonValueBinary::IsBinary(JsonValue)) + { + auto BinaryArray = FJsonValueBinary::AsBinary(JsonValue); + return sio::binary_message::create(std::make_shared((char*)BinaryArray.GetData(), BinaryArray.Num())); + } + else + { + return sio::string_message::create(StdString(JsonValue->AsString())); + } + } + else if (JsonValue->Type == EJson::Number) + { + return sio::double_message::create(JsonValue->AsNumber()); + } + else if (JsonValue->Type == EJson::Boolean) + { + return sio::bool_message::create(JsonValue->AsBool()); + } + else if (JsonValue->Type == EJson::Array) + { + auto ValueArray = JsonValue->AsArray(); + auto ArrayMessage = sio::array_message::create(); + + for (auto ItemValue : ValueArray) + { + //must use get_vector() for each + ArrayMessage->get_vector().push_back(ToSIOMessage(ItemValue)); + } + + return ArrayMessage; + } + else if (JsonValue->Type == EJson::Object) + { + auto ValueTmap = JsonValue->AsObject()->Values; + + auto ObjectMessage = sio::object_message::create(); + + for (auto ItemPair : ValueTmap) + { + //important to use get_map() directly to insert the key in the correct map and not a pointer copy + ObjectMessage->get_map()[StdString(ItemPair.Key)] = ToSIOMessage(ItemPair.Value); + } + + return ObjectMessage; + } + else + { + return sio::null_message::create(); + } +} + +std::string USIOMessageConvert::StdString(FString UEString) +{ + return std::string(TCHAR_TO_UTF8(*UEString)); //TCHAR_TO_ANSI try this string instead? +} + +FString USIOMessageConvert::FStringFromStd(std::string StdString) +{ + return FString(StdString.c_str()); +} \ No newline at end of file diff --git a/Source/SocketIOClient/Private/SocketIOClient.cpp b/Source/SocketIOClient/Private/SocketIOClient.cpp index b56e8a8d..c5f5557e 100644 --- a/Source/SocketIOClient/Private/SocketIOClient.cpp +++ b/Source/SocketIOClient/Private/SocketIOClient.cpp @@ -2,6 +2,8 @@ #include "SocketIOClientPrivatePCH.h" + + #define LOCTEXT_NAMESPACE "FSocketIOClientModule" void FSocketIOClientModule::StartupModule() diff --git a/Source/SocketIOClient/Private/SocketIOClientComponent.cpp b/Source/SocketIOClient/Private/SocketIOClientComponent.cpp index f7008abf..3b1de490 100644 --- a/Source/SocketIOClient/Private/SocketIOClientComponent.cpp +++ b/Source/SocketIOClient/Private/SocketIOClientComponent.cpp @@ -3,6 +3,7 @@ #include "SocketIOClientPrivatePCH.h" #include "SocketIOClientComponent.h" #include "SIOLambdaRunnable.h" +#include "SIOJConvert.h" USocketIOClientComponent::USocketIOClientComponent(const FObjectInitializer &init) : UActorComponent(init) @@ -14,21 +15,86 @@ USocketIOClientComponent::USocketIOClientComponent(const FObjectInitializer &ini SessionId = FString(TEXT("invalid")); } -std::string USocketIOClientComponent::StdString(FString UEString) +void USocketIOClientComponent::InitializeComponent() { - return std::string(TCHAR_TO_UTF8(*UEString)); //TCHAR_TO_ANSI try this string instead? + Super::InitializeComponent(); + if (ShouldAutoConnect) + { + Connect(AddressAndPort); //connect to default address + } } -FString USocketIOClientComponent::FStringFromStd(std::string StdString) + +void USocketIOClientComponent::UninitializeComponent() { - return FString(StdString.c_str()); + SyncDisconnect(); + Super::UninitializeComponent(); } -void USocketIOClientComponent::Connect(FString InAddressAndPort) +bool USocketIOClientComponent::CallBPFunctionWithResponse(UObject* Target, const FString& FunctionName, TArray> Response) +{ + UFunction* Function = Target->FindFunction(FName(*FunctionName)); + if (nullptr == Function) + { + UE_LOG(SocketIOLog, Warning, TEXT("CallFunctionByNameWithArguments: Function not found '%s'"), *FunctionName); + return false; + } + + auto ResponseJsonValue = USIOJConvert::ToSIOJsonValue(Response); + + struct FDynamicArgs + { + USIOJsonValue* Arg01 = NULL; + USIOJsonValue* Arg02 = NULL; + }; + + //create the container + FDynamicArgs Args = FDynamicArgs(); + + //convenience wrapper, response is a single object + Args.Arg01 = NewObject(); + Args.Arg01->SetRootValue(Response[0]); + + //add the full response array as second param + Args.Arg02 = ResponseJsonValue; + + //Call the function + Target->ProcessEvent(Function, &Args); + + return true; +} + +bool USocketIOClientComponent::CallBPFunctionWithMessage(UObject* Target, const FString& FunctionName, TSharedPtr Message) +{ + UFunction* Function = Target->FindFunction(FName(*FunctionName)); + if (nullptr == Function) + { + UE_LOG(SocketIOLog, Warning, TEXT("CallFunctionByNameWithArguments: Function not found '%s'"), *FunctionName); + return false; + } + + struct FDynamicArgs + { + USIOJsonValue* Arg01 = NULL; + }; + FDynamicArgs Args = FDynamicArgs(); + + Args.Arg01 = NewObject(); + Args.Arg01->SetRootValue(Message); + + //Call the function + Target->ProcessEvent(Function, &Args); + + return true; +} + +#pragma region Connect + +void USocketIOClientComponent::Connect(const FString& InAddressAndPort) { - std::string StdAddressString = StdString(InAddressAndPort); + std::string StdAddressString = USIOMessageConvert::StdString(InAddressAndPort); if (InAddressAndPort.IsEmpty()) { - StdAddressString = StdString(AddressAndPort); + StdAddressString = USIOMessageConvert::StdString(AddressAndPort); } //Connect to the server on a background thread @@ -37,35 +103,35 @@ void USocketIOClientComponent::Connect(FString InAddressAndPort) //Attach the specific connection status events events PrivateClient.set_open_listener(sio::client::con_listener([&]() { - SessionId = FStringFromStd(PrivateClient.get_sessionid()); - UE_LOG(LogTemp, Log, TEXT("SocketIO Connected with session: %s"), *SessionId); + SessionId = USIOMessageConvert::FStringFromStd(PrivateClient.get_sessionid()); + UE_LOG(SocketIOLog, Log, TEXT("SocketIO Connected with session: %s"), *SessionId); OnConnected.Broadcast(SessionId); })); PrivateClient.set_close_listener(sio::client::close_listener([&](sio::client::close_reason const& reason) { SessionId = FString(TEXT("invalid")); - UE_LOG(LogTemp, Log, TEXT("SocketIO Disconnected")); + UE_LOG(SocketIOLog, Log, TEXT("SocketIO Disconnected")); OnDisconnected.Broadcast((EConnectionCloseReason)reason); })); PrivateClient.set_socket_open_listener(sio::client::socket_listener([&](std::string const& nsp) { - FString Namespace = FStringFromStd(nsp); - UE_LOG(LogTemp, Log, TEXT("SocketIO connected to namespace: %s"), *Namespace); + FString Namespace = USIOMessageConvert::FStringFromStd(nsp); + UE_LOG(SocketIOLog, Log, TEXT("SocketIO connected to namespace: %s"), *Namespace); OnSocketNamespaceConnected.Broadcast(Namespace); })); PrivateClient.set_socket_close_listener(sio::client::socket_listener([&](std::string const& nsp) { - FString Namespace = FStringFromStd(nsp); - UE_LOG(LogTemp, Log, TEXT("SocketIO disconnected from namespace: %s"), *Namespace); - OnSocketNamespaceDisconnected.Broadcast(FStringFromStd(nsp)); + FString Namespace = USIOMessageConvert::FStringFromStd(nsp); + UE_LOG(SocketIOLog, Log, TEXT("SocketIO disconnected from namespace: %s"), *Namespace); + OnSocketNamespaceDisconnected.Broadcast(USIOMessageConvert::FStringFromStd(nsp)); })); PrivateClient.set_fail_listener(sio::client::con_listener([&]() { - UE_LOG(LogTemp, Log, TEXT("SocketIO failed to connect.")); + UE_LOG(SocketIOLog, Log, TEXT("SocketIO failed to connect.")); OnFail.Broadcast(); })); @@ -75,6 +141,14 @@ void USocketIOClientComponent::Connect(FString InAddressAndPort) void USocketIOClientComponent::Disconnect() +{ + FSIOLambdaRunnable::RunLambdaOnBackGroundThread([&] + { + SyncDisconnect(); + }); +} + +void USocketIOClientComponent::SyncDisconnect() { if (PrivateClient.opened()) { @@ -84,135 +158,180 @@ void USocketIOClientComponent::Disconnect() } } -void USocketIOClientComponent::Emit(FString Name, FString Data, FString Namespace /* = FString(TEXT("/"))*/) +#pragma endregion Connect + +#pragma region Emit + +void USocketIOClientComponent::Emit(const FString& EventName, USIOJsonValue* Message, const FString& Namespace /*= FString(TEXT("/"))*/) { - PrivateClient.socket(StdString(Namespace))->emit(StdString(Name), StdString(Data)); - //UE_LOG(LogTemp, Log, TEXT("Emit %s with %s"), *Name, *Data); + PrivateClient.socket(USIOMessageConvert::StdString(Namespace))->emit( + USIOMessageConvert::StdString(EventName), + USIOMessageConvert::ToSIOMessage(Message->GetRootValue())); } +void USocketIOClientComponent::EmitWithCallBack(const FString& EventName, USIOJsonValue* Message /*= nullptr*/, const FString& CallbackFunctionName /*= FString(TEXT(""))*/, UObject* Target /*= nullptr*/, const FString& Namespace /*= FString(TEXT("/"))*/) +{ + if (!CallbackFunctionName.IsEmpty()) + { + if (Target == nullptr) + { + Target = GetOwner(); + } + + //Set the message is not null + TSharedPtr JsonMessage = nullptr; + if (Message != nullptr) + { + JsonMessage = Message->GetRootValue(); + } + else + { + JsonMessage = MakeShareable(new FJsonValueNull); + } + + EmitNative(EventName, JsonMessage, [&, Target, CallbackFunctionName, this](auto Response) + { + CallBPFunctionWithResponse(Target, CallbackFunctionName, Response); + }, Namespace); + } + else + { + EmitNative(EventName, Message->GetRootValue(),nullptr,Namespace); + } +} -void USocketIOClientComponent::EmitBuffer(FString Name, uint8* Data, int32 DataLength, FString Namespace /*= FString(TEXT("/"))*/) +void USocketIOClientComponent::EmitNative(const FString& EventName, const TSharedPtr& Message /*= nullptr*/, TFunction< void(const TArray>&)> CallbackFunction /*= nullptr*/, const FString& Namespace /*= FString(TEXT("/"))*/) { - PrivateClient.socket(StdString(Namespace))->emit(StdString(Name), std::make_shared((char*)Data, DataLength)); + const auto SafeCallback = CallbackFunction; + EmitRaw( + EventName, + USIOMessageConvert::ToSIOMessage(Message), + [&, SafeCallback](const sio::message::list& MessageList) + { + TArray> ValueArray; + + for (int i = 0; i < MessageList.size(); i++) + { + auto ItemMessagePtr = MessageList[i]; + ValueArray.Add(USIOMessageConvert::ToJsonValue(ItemMessagePtr)); + } + + SafeCallback(ValueArray); + }, Namespace); } -void USocketIOClientComponent::EmitRaw(FString Name, const sio::message::list& MessageList, FString Namespace) +void USocketIOClientComponent::EmitNative(const FString& EventName, const TSharedPtr& ObjectMessage /*= nullptr*/, TFunction< void(const TArray>&)> CallbackFunction /*= nullptr*/, const FString& Namespace /*= FString(TEXT("/"))*/) { - PrivateClient.socket(StdString(Namespace))->emit(StdString(Name), MessageList); + EmitNative(EventName, MakeShareable(new FJsonValueObject(ObjectMessage)), CallbackFunction, Namespace); } -//todo: collapse all of this into a single usable Emit -void USocketIOClientComponent::EmitRawWithCallback(FString Name, const sio::message::list& MessageList, TFunction ResponseFunction, FString Namespace) +void USocketIOClientComponent::EmitNative(const FString& EventName, const FString& StringMessage /*= FString()*/, TFunction< void(const TArray>&)> CallbackFunction /*= nullptr*/, const FString& Namespace /*= FString(TEXT("/"))*/) { - const TFunction SafeFunction = ResponseFunction; + EmitNative(EventName, MakeShareable(new FJsonValueString(StringMessage)), CallbackFunction, Namespace); +} - PrivateClient.socket(StdString(Namespace))->emit(StdString(Name), MessageList, [&, SafeFunction](const sio::message::list& response) { - //Call on gamethread - FFunctionGraphTask::CreateAndDispatchWhenReady([&, SafeFunction, response] - { - SafeFunction(response); - }, TStatId(), nullptr, ENamedThreads::GameThread); - }); +void USocketIOClientComponent::EmitNative(const FString& EventName, double NumberMessage, TFunction< void(const TArray>&)> CallbackFunction /*= nullptr*/, const FString& Namespace /*= FString(TEXT("/"))*/) +{ + EmitNative(EventName, MakeShareable(new FJsonValueNumber(NumberMessage)), CallbackFunction, Namespace); } -void USocketIOClientComponent::BindEvent(FString Name, FString Namespace /*= FString(TEXT("/"))*/) +void USocketIOClientComponent::EmitNative(const FString& EventName, const TArray& BinaryMessage, TFunction< void(const TArray>&)> CallbackFunction /*= nullptr*/, const FString& Namespace /*= FString(TEXT("/"))*/) { - BindStringMessageLambdaToEvent([&](const FString& EventName, const FString& EventData) - { - On.Broadcast(EventName, EventData); - }, Name, Namespace); + EmitNative(EventName, MakeShareable(new FJsonValueBinary(BinaryMessage)), CallbackFunction, Namespace); } +void USocketIOClientComponent::EmitNative(const FString& EventName, const TArray>& ArrayMessage, TFunction< void(const TArray>&)> CallbackFunction /*= nullptr*/, const FString& Namespace /*= FString(TEXT("/"))*/) +{ + EmitNative(EventName, MakeShareable(new FJsonValueArray(ArrayMessage)), CallbackFunction, Namespace); +} -void USocketIOClientComponent::InitializeComponent() +void USocketIOClientComponent::EmitNative(const FString& EventName, bool BooleanMessage, TFunction< void(const TArray>&)> CallbackFunction /*= nullptr*/, const FString& Namespace /*= FString(TEXT("/"))*/) { - Super::InitializeComponent(); - if (ShouldAutoConnect) - { - Connect(AddressAndPort); //connect to default address - } + EmitNative(EventName, MakeShareable(new FJsonValueBoolean(BooleanMessage)), CallbackFunction, Namespace); } -void USocketIOClientComponent::UninitializeComponent() +void USocketIOClientComponent::EmitNative(const FString& EventName, UStruct* Struct, const void* StructPtr, TFunction< void(const TArray>&)> CallbackFunction /*= nullptr*/, const FString& Namespace /*= FString(TEXT("/"))*/) { - Disconnect(); - Super::UninitializeComponent(); + EmitNative(EventName, USIOJConvert::ToJsonObject(Struct, (void*)StructPtr), CallbackFunction, Namespace); } -void USocketIOClientComponent::BindLambdaToEvent(TFunction< void()> InFunction, FString Name, FString Namespace /*= FString(TEXT("/"))*/) +void USocketIOClientComponent::EmitRaw(const FString& EventName, const sio::message::list& MessageList, TFunction ResponseFunction, const FString& Namespace) { - const TFunction< void()> SafeFunction = InFunction; //copy the function so it remains in context + const TFunction SafeFunction = ResponseFunction; - PrivateClient.socket(StdString(Namespace))->on( - StdString(Name), - sio::socket::event_listener_aux( - [&, SafeFunction](std::string const& name, sio::message::ptr const& data, bool isAck, sio::message::list &ack_resp) + PrivateClient.socket(USIOMessageConvert::StdString(Namespace))->emit( + USIOMessageConvert::StdString(EventName), + MessageList, + [&, SafeFunction](const sio::message::list& response) { - FFunctionGraphTask::CreateAndDispatchWhenReady([&, SafeFunction] + if (SafeFunction != nullptr) { - SafeFunction(); - }, TStatId(), nullptr, ENamedThreads::GameThread); - })); + //Callback on game thread + FFunctionGraphTask::CreateAndDispatchWhenReady([&, SafeFunction, response] + { + SafeFunction(response); + }, TStatId(), nullptr, ENamedThreads::GameThread); + } + }); } -void USocketIOClientComponent::BindStringMessageLambdaToEvent( - TFunction< void(const FString&, const FString&)> InFunction, - FString Name, FString Namespace /*= FString(TEXT("/"))*/) +void USocketIOClientComponent::EmitRawBinary(const FString& EventName, uint8* Data, int32 DataLength, const FString& Namespace /*= FString(TEXT("/"))*/) { - const TFunction< void(const FString&, const FString&)> SafeFunction = InFunction; //copy the function so it remains in context + PrivateClient.socket(USIOMessageConvert::StdString(Namespace))->emit(USIOMessageConvert::StdString(EventName), std::make_shared((char*)Data, DataLength)); +} - PrivateClient.socket(StdString(Namespace))->on( - StdString(Name), - sio::socket::event_listener_aux( - [&, SafeFunction](std::string const& name, sio::message::ptr const& data, bool isAck, sio::message::list &ack_resp) +#pragma endregion Emit + +#pragma region OnEvents + +void USocketIOClientComponent::BindEvent(const FString& EventName, const FString& Namespace) +{ + OnRawEvent(EventName, [&](const FString& Event, const sio::message::ptr& RawMessage) { + USIOJsonValue* NewValue = NewObject(); + auto Value = USIOMessageConvert::ToJsonValue(RawMessage); + NewValue->SetRootValue(Value); + On.Broadcast(Event, NewValue); + + }, Namespace); +} + +void USocketIOClientComponent::BindEventToFunction(const FString& EventName, const FString& FunctionName, UObject* Target, const FString& Namespace /*= FString(TEXT("/"))*/) +{ + if (!FunctionName.IsEmpty()) { - const FString SafeName = FStringFromStd(name); - FString TempData; - - //Convert number messages into strings, only supported bypass for now - if (data->get_flag() == data->flag_string) - { - TempData = FStringFromStd(data->get_string()); - } - else if (data->get_flag() == data->flag_integer) - { - TempData = FStringFromStd(std::to_string(data->get_int())); - } - else if (data->get_flag() == data->flag_double) - { - TempData = FStringFromStd(std::to_string(data->get_double())); - } - else if (data->get_flag() == data->flag_boolean) - { - TempData = FStringFromStd(std::to_string(data->get_bool())); - } - else + if (Target == nullptr) { - TempData = FString(TEXT("")); - UE_LOG(LogTemp, Warning, TEXT("SocketIOClientComponent Unsupported data type for BindStringMessageLambdaToEvent, use C++ events for now.")); + Target = GetOwner(); } - - //Todo: modify data->get_string() to stringify if data is an object or binary - const FString& SafeData = TempData; - - FFunctionGraphTask::CreateAndDispatchWhenReady([&, SafeFunction, SafeName, SafeData] + OnNativeEvent(EventName, [&, FunctionName, Target](const FString& Event, const TSharedPtr& Message) { - SafeFunction(SafeName, SafeData); - }, TStatId(), nullptr, ENamedThreads::GameThread); - })); + CallBPFunctionWithMessage(Target, FunctionName, Message); + }, Namespace); + } + else + { + //if we forgot our function name, fallback to regular bind event + BindEvent(EventName, Namespace); + } } +void USocketIOClientComponent::OnNativeEvent(const FString& EventName, TFunction< void(const FString&, const TSharedPtr&)> CallbackFunction, const FString& Namespace /*= FString(TEXT("/"))*/) +{ + OnRawEvent(EventName, [&, CallbackFunction](const FString& Event, const sio::message::ptr& RawMessage) { + CallbackFunction(Event, USIOMessageConvert::ToJsonValue(RawMessage)); + }, Namespace); +} -void USocketIOClientComponent::BindRawMessageLambdaToEvent(TFunction< void(const FString&, const sio::message::ptr&)> InFunction, FString Name, FString Namespace /*= FString(TEXT("/"))*/) +void USocketIOClientComponent::OnRawEvent(const FString& EventName, TFunction< void(const FString&, const sio::message::ptr&)> CallbackFunction, const FString& Namespace /*= FString(TEXT("/"))*/) { - const TFunction< void(const FString&, const sio::message::ptr&)> SafeFunction = InFunction; //copy the function so it remains in context + const TFunction< void(const FString&, const sio::message::ptr&)> SafeFunction = CallbackFunction; //copy the function so it remains in context - PrivateClient.socket(StdString(Namespace))->on( - StdString(Name), + PrivateClient.socket(USIOMessageConvert::StdString(Namespace))->on( + USIOMessageConvert::StdString(EventName), sio::socket::event_listener_aux( [&, SafeFunction](std::string const& name, sio::message::ptr const& data, bool isAck, sio::message::list &ack_resp) { - const FString SafeName = FStringFromStd(name); + const FString SafeName = USIOMessageConvert::FStringFromStd(name); FFunctionGraphTask::CreateAndDispatchWhenReady([&, SafeFunction, SafeName, data] { @@ -222,16 +341,16 @@ void USocketIOClientComponent::BindRawMessageLambdaToEvent(TFunction< void(const } -void USocketIOClientComponent::BindBinaryMessageLambdaToEvent(TFunction< void(const FString&, const TArray&)> InFunction, FString Name, FString Namespace /*= FString(TEXT("/"))*/) +void USocketIOClientComponent::OnBinaryEvent(const FString& EventName, TFunction< void(const FString&, const TArray&)> CallbackFunction, const FString& Namespace /*= FString(TEXT("/"))*/) { - const TFunction< void(const FString&, const TArray&)> SafeFunction = InFunction; //copy the function so it remains in context + const TFunction< void(const FString&, const TArray&)> SafeFunction = CallbackFunction; //copy the function so it remains in context - PrivateClient.socket(StdString(Namespace))->on( - StdString(Name), + PrivateClient.socket(USIOMessageConvert::StdString(Namespace))->on( + USIOMessageConvert::StdString(EventName), sio::socket::event_listener_aux( [&, SafeFunction](std::string const& name, sio::message::ptr const& data, bool isAck, sio::message::list &ack_resp) { - const FString SafeName = FStringFromStd(name); + const FString SafeName = USIOMessageConvert::FStringFromStd(name); //Construct raw buffer if (data->get_flag() == sio::message::flag_binary) @@ -248,58 +367,9 @@ void USocketIOClientComponent::BindBinaryMessageLambdaToEvent(TFunction< void(co } else { - UE_LOG(LogTemp, Warning, TEXT("Non-binary message received to binary message lambda, check server message data!")); + UE_LOG(SocketIOLog, Warning, TEXT("Non-binary message received to binary message lambda, check server message data!")); } })); } -//Todo: add object -> json conversion, or convert it to a UEJSON object that can stringify - -sio::message::ptr USocketIOClientComponent::getMessage(const std::string& json) -{ - //std::lock_guard< std::mutex > guard(packetLock); - sio::message::ptr message; - /*manager.set_decode_callback([&](sio::packet const& p) - { - message = p.get_message(); - }); - - // Magic message type / ID - std::string payload = std::string("42") + json; - manager.put_payload(payload); - - manager.reset();*/ - return message; -} - -std::string USocketIOClientComponent::getJson(sio::message::ptr msg) -{ - //std::lock_guard< std::mutex > guard(packetLock); - /*std::stringstream ss; - sio::packet packet("/", msg); - manager.encode(packet, [&](bool isBinary, std::shared_ptr const& json) - { - ss << *json; - assert(!isBinary); - }); - manager.reset(); - - // Need to strip off the message type flags (typically '42', - // but there are other possible combinations). - std::string result = ss.str(); - std::size_t indexList = result.find('['); - std::size_t indexObject = result.find('{'); - std::size_t indexString = result.find('"'); - std::size_t index = indexList; - if (indexObject != std::string::npos && indexObject < index) - index = indexObject; - if (indexString != std::string::npos && indexString < index) - index = indexString; - - if (index == std::string::npos) { - UE_LOG(LogTemp, Log, TEXT("Error decoding json object\n Body: %s"), result); - return ""; - } - return result;//.substr(index);*/ - return std::string(); -} +#pragma endregion OnEvents \ No newline at end of file diff --git a/Source/SocketIOClient/Private/SocketIOClientPrivatePCH.h b/Source/SocketIOClient/Private/SocketIOClientPrivatePCH.h index 144f5861..a52780ba 100644 --- a/Source/SocketIOClient/Private/SocketIOClientPrivatePCH.h +++ b/Source/SocketIOClient/Private/SocketIOClientPrivatePCH.h @@ -5,6 +5,11 @@ #include "Core.h" #include "Engine.h" #include "Object.h" +#include "Json.h" +#include "JsonUtilities.h" +#include "SIOJsonValue.h" +#include "sio_client.h" +#include "SIOMessageConvert.h" // You should place include statements to your module's private header files here. You only need to // add includes for headers that are used in most of your module's source files though. \ No newline at end of file diff --git a/Source/SocketIOClient/Public/SIOMessageConvert.h b/Source/SocketIOClient/Public/SIOMessageConvert.h new file mode 100644 index 00000000..c58df33a --- /dev/null +++ b/Source/SocketIOClient/Public/SIOMessageConvert.h @@ -0,0 +1,29 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "Object.h" +#include "sio_client.h" +#include "SIOMessageConvert.generated.h" + +DECLARE_LOG_CATEGORY_EXTERN(SocketIOLog, Log, All); + +/** + * + */ +UCLASS() +class SOCKETIOCLIENT_API USIOMessageConvert : public UObject +{ + GENERATED_BODY() +public: + + //To from: + + //sio::message <-> FJsonValue + static TSharedPtr ToJsonValue(const sio::message::ptr& Message); + static sio::message::ptr ToSIOMessage(const TSharedPtr& JsonValue); + + //std::string <-> FString + static std::string StdString(FString UEString); + static FString FStringFromStd(std::string StdString); +}; diff --git a/Source/SocketIOClient/Public/SocketIOClient.h b/Source/SocketIOClient/Public/SocketIOClient.h index a1d96ad7..536b2632 100644 --- a/Source/SocketIOClient/Public/SocketIOClient.h +++ b/Source/SocketIOClient/Public/SocketIOClient.h @@ -4,6 +4,7 @@ #include "ModuleManager.h" + class SOCKETIOCLIENT_API FSocketIOClientModule : public IModuleInterface { public: diff --git a/Source/SocketIOClient/Public/SocketIOClientComponent.h b/Source/SocketIOClient/Public/SocketIOClientComponent.h index 7019a43b..91744b23 100644 --- a/Source/SocketIOClient/Public/SocketIOClientComponent.h +++ b/Source/SocketIOClient/Public/SocketIOClientComponent.h @@ -1,6 +1,9 @@ #pragma once #include "sio_client.h" +#include "SIOJsonObject.h" +#include "SIOJsonValue.h" +#include "SIOJConvert.h" #include "Components/ActorComponent.h" #include "SocketIOClientComponent.generated.h" @@ -41,7 +44,7 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE(FSIOCEventSignature); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSIOCSocketEventSignature, FString, Namespace); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSIOCOpenEventSignature, FString, SessionId); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSIOCCloseEventSignature, TEnumAsByte, Reason); -DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FSIOCNameDataEventSignature, FString, Name, FString, Data); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FSIOCEventJsonSignature, FString, Event, class USIOJsonValue*, MessageJson); UCLASS(ClassGroup = "Networking", meta = (BlueprintSpawnableComponent)) class SOCKETIOCLIENT_API USocketIOClientComponent : public UActorComponent @@ -50,98 +53,287 @@ class SOCKETIOCLIENT_API USocketIOClientComponent : public UActorComponent public: //Async events + + /** Event received on socket.io connection established. */ UPROPERTY(BlueprintAssignable, Category = "SocketIO Events") FSIOCOpenEventSignature OnConnected; + /** Event received on socket.io connection disconnected. */ UPROPERTY(BlueprintAssignable, Category = "SocketIO Events") FSIOCCloseEventSignature OnDisconnected; + /** Event received on having joined namespace. */ UPROPERTY(BlueprintAssignable, Category = "SocketIO Events") FSIOCSocketEventSignature OnSocketNamespaceConnected; + /** Event received on having left namespace. */ UPROPERTY(BlueprintAssignable, Category = "SocketIO Events") FSIOCSocketEventSignature OnSocketNamespaceDisconnected; + /** Event received on connection failure. */ UPROPERTY(BlueprintAssignable, Category = "SocketIO Events") FSIOCEventSignature OnFail; + /** On bound event received. */ UPROPERTY(BlueprintAssignable, Category = "SocketIO Events") - FSIOCNameDataEventSignature On; + FSIOCEventJsonSignature On; - //Default properties - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = defaults) + /** Default connection address string in form e.g. http://localhost:80. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SocketIO Properties") FString AddressAndPort; - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = defaults) + /** If true will auto-connect on begin play to address specified in AddressAndPort. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SocketIO Properties") bool ShouldAutoConnect; + /** When connected this session id will be valid and contain a unique Id. */ UPROPERTY(BlueprintReadWrite, Category = "SocketIO Properties") FString SessionId; /** - * Connect to a socket.io server, optional if auto-connect is set + * Connect to a socket.io server, optional method if auto-connect is set to true. * * @param AddressAndPort the address in URL format with port */ UFUNCTION(BlueprintCallable, Category = "SocketIO Functions") - void Connect(FString InAddressAndPort); + void Connect(const FString& InAddressAndPort); + /** + * Disconnect from current socket.io server, optional method. + * + * @param AddressAndPort the address in URL format with port + */ UFUNCTION(BlueprintCallable, Category = "SocketIO Functions") void Disconnect(); + void SyncDisconnect(); + + // + //Blueprint Functions + // + /** - * Emit a string event with a string action + * Emit an event with a JsonValue message * - * @param Name Event name - * @param Data Data string + * @param Name Event name + * @param Message SIOJJsonValue + * @param Namespace Namespace within socket.io */ UFUNCTION(BlueprintCallable, Category = "SocketIO Functions") - void Emit(FString Name, FString Data = FString(TEXT("")), FString Namespace = FString(TEXT("/"))); + void Emit(const FString& EventName, USIOJsonValue* Message = nullptr, const FString& Namespace = FString(TEXT("/"))); - //Binary data version, only available in C++ - void EmitBuffer(FString Name, uint8* Data, int32 DataLength, FString Namespace = FString(TEXT("/"))); + /** + * Emit an event with a JsonValue message with a callback function defined by CallBackFunctionName + * + * @param Name Event name + * @param Message SIOJsonValue + * @param CallbackFunctionName Name of the optional callback function with signature (String, SIOJsonValue) + * @param Target CallbackFunction target object, typically self where this is called. + * @param Namespace Namespace within socket.io + */ + UFUNCTION(BlueprintCallable, Category = "SocketIO Functions") + void EmitWithCallBack( const FString& EventName, + USIOJsonValue* Message = nullptr, + const FString& CallbackFunctionName = FString(""), + UObject* Target = nullptr, + const FString& Namespace = FString(TEXT("/"))); - //Raw sio::message emit, only available in C++ - void EmitRaw(FString Name, const sio::message::list& MessageList = nullptr, FString Namespace = FString(TEXT("/"))); - void EmitRawWithCallback(FString Name, const sio::message::list& MessageList = nullptr, TFunction ResponseFunction = nullptr, FString Namespace = FString(TEXT("/"))); - /** - * Emit a string event with a string action + * Bind an event, then respond to it with 'On' multi-cast delegate * - * @param Name Event name + * @param EventName Event name * @param Namespace Optional namespace, defaults to default namespace */ UFUNCTION(BlueprintCallable, Category = "SocketIO Functions") - void BindEvent(FString Name, FString Namespace = FString(TEXT("/"))); + void BindEvent(const FString& EventName, const FString& Namespace = FString(TEXT("/"))); - virtual void InitializeComponent() override; - virtual void UninitializeComponent() override; + /** + * Bind an event to a function with the given name. + * Expects a String message signature which can be decoded from JSON into SIOJsonObject + * + * @param EventName Event name + * @param FunctionName The function that gets called when the event is received + * @param Target Optional, defaults to owner. Change to delegate to another class. + */ + UFUNCTION(BlueprintCallable, Category = "SocketIO Functions") + void BindEventToFunction( const FString& EventName, + const FString& FunctionName, + UObject* Target, + const FString& Namespace = FString(TEXT("/"))); + // + //C++ functions + // + + /** + * Emit an event with a JsonValue message + * + * @param EventName Event name + * @param Message FJsonValue + * @param CallbackFunction Optional callback TFunction + * @param Namespace Optional Namespace within socket.io + */ + void EmitNative(const FString& EventName, + const TSharedPtr& Message = nullptr, + TFunction< void(const TArray>&)> CallbackFunction = nullptr, + const FString& Namespace = FString(TEXT("/"))); + + /** + * (Overloaded) Emit an event with a Json Object message + * + * @param EventName Event name + * @param ObjectMessage FJsonObject + * @param CallbackFunction Optional callback TFunction + * @param Namespace Optional Namespace within socket.io + */ + void EmitNative(const FString& EventName, + const TSharedPtr& ObjectMessage = nullptr, + TFunction< void(const TArray>&)> CallbackFunction = nullptr, + const FString& Namespace = FString(TEXT("/"))); - //C++ version of binding arbitrary lambda functions to events, if you want only to be notified and don't care about arguments - void BindLambdaToEvent(TFunction< void()> InFunction, FString Name, FString Namespace = FString(TEXT("/"))); + /** + * (Overloaded) Emit an event with a string message + * + * @param EventName Event name + * @param StringMessage Message in string format + * @param CallbackFunction Optional callback TFunction + * @param Namespace Optional Namespace within socket.io + */ + void EmitNative(const FString& EventName, + const FString& StringMessage = FString(), + TFunction< void(const TArray>&)> CallbackFunction = nullptr, + const FString& Namespace = FString(TEXT("/"))); - //When you care about the data you get - void BindStringMessageLambdaToEvent(TFunction< void(const FString&, const FString&)> InFunction, FString Name, FString Namespace = FString(TEXT("/"))); - void BindBinaryMessageLambdaToEvent(TFunction< void(const FString&, const TArray&)> InFunction, FString Name, FString Namespace = FString(TEXT("/"))); + /** + * (Overloaded) Emit an event with a number (double) message + * + * @param EventName Event name + * @param NumberMessage Message in double format + * @param CallbackFunction Optional callback TFunction + * @param Namespace Optional Namespace within socket.io + */ + void EmitNative(const FString& EventName, + double NumberMessage, + TFunction< void(const TArray>&)> CallbackFunction = nullptr, + const FString& Namespace = FString(TEXT("/"))); - //Raw sio::message lambda - void BindRawMessageLambdaToEvent(TFunction< void(const FString&, const sio::message::ptr&)> InFunction, FString Name, FString Namespace = FString(TEXT("/"))); + /** + * (Overloaded) Emit an event with a bool message + * + * @param EventName Event name + * @param BooleanMessage Message in bool format + * @param CallbackFunction Optional callback TFunction + * @param Namespace Optional Namespace within socket.io + */ + void EmitNative(const FString& EventName, + bool BooleanMessage, + TFunction< void(const TArray>&)> CallbackFunction = nullptr, + const FString& Namespace = FString(TEXT("/"))); + + /** + * (Overloaded) Emit an event with a binary message + * + * @param EventName Event name + * @param BinaryMessage Message in an TArray of uint8 + * @param CallbackFunction Optional callback TFunction + * @param Namespace Optional Namespace within socket.io + */ + void EmitNative(const FString& EventName, + const TArray& BinaryMessage, + TFunction< void(const TArray>&)> CallbackFunction = nullptr, + const FString& Namespace = FString(TEXT("/"))); + + /** + * (Overloaded) Emit an event with an array message + * + * @param EventName Event name + * @param ArrayMessage Message in an TArray of FJsonValues + * @param CallbackFunction Optional callback TFunction + * @param Namespace Optional Namespace within socket.io + */ + void EmitNative(const FString& EventName, + const TArray>& ArrayMessage, + TFunction< void(const TArray>&)> CallbackFunction = nullptr, + const FString& Namespace = FString(TEXT("/"))); + + /** + * (Overloaded) Emit an event with an UStruct message + * + * @param EventName Event name + * @param Struct UStruct type usually obtained via e.g. FMyStructType::StaticStruct() + * @param StructPtr Pointer to the actual struct memory e.g. &MyStruct + * @param CallbackFunction Optional callback TFunction + * @param Namespace Optional Namespace within socket.io + */ + void EmitNative(const FString& EventName, + UStruct* Struct, + const void* StructPtr, + TFunction< void(const TArray>&)> CallbackFunction = nullptr, + const FString& Namespace = FString(TEXT("/"))); + + /** + * Emit a raw sio::message event + * + * @param EventName Event name + * @param MessageList Message in sio::message::list format + * @param CallbackFunction Optional callback TFunction with raw signature + * @param Namespace Optional Namespace within socket.io + */ + void EmitRaw( const FString& EventName, + const sio::message::list& MessageList = nullptr, + TFunction CallbackFunction = nullptr, + const FString& Namespace = FString(TEXT("/"))); + + /** + * Emit an optimized binary message + * + * @param EventName Event name + * @param Data Buffer Pointer + * @param DataLength Buffer size + * @param Namespace Optional Namespace within socket.io + */ + void EmitRawBinary(const FString& EventName, uint8* Data, int32 DataLength, const FString& Namespace = FString(TEXT("/"))); + /** + * Call function callback on receiving socket event. C++ only. + * + * @param EventName Event name + * @param TFunction Lambda callback, JSONValue + * @param Namespace Optional namespace, defaults to default namespace + */ + void OnNativeEvent( const FString& EventName, + TFunction< void(const FString&, const TSharedPtr&)> CallbackFunction, + const FString& Namespace = FString(TEXT("/"))); + /** + * Call function callback on receiving raw event. C++ only. + * + * @param EventName Event name + * @param TFunction Lambda callback, raw flavor + * @param Namespace Optional namespace, defaults to default namespace + */ + void OnRawEvent(const FString& EventName, + TFunction< void(const FString&, const sio::message::ptr&)> CallbackFunction, + const FString& Namespace = FString(TEXT("/"))); + /** + * Call function callback on receiving binary event. C++ only. + * + * @param EventName Event name + * @param TFunction Lambda callback, raw flavor + * @param Namespace Optional namespace, defaults to default namespace + */ + void OnBinaryEvent( const FString& EventName, + TFunction< void(const FString&, const TArray&)> CallbackFunction, + const FString& Namespace = FString(TEXT("/"))); - //Convenience conversion - static std::string StdString(FString UEString); - static FString FStringFromStd(std::string StdString); + virtual void InitializeComponent() override; + virtual void UninitializeComponent() override; protected: - sio::client PrivateClient; -private: + bool CallBPFunctionWithResponse(UObject* Target, const FString& FunctionName, TArray> Response); + bool CallBPFunctionWithMessage(UObject* Target, const FString& FunctionName, TSharedPtr Message); - //sio::packet_manager manager; - //std::mutex packetLock; - sio::message::ptr getMessage(const std::string& json); - std::string getJson(sio::message::ptr msg); + sio::client PrivateClient; }; \ No newline at end of file diff --git a/Source/SocketIOClient/SocketIOClient.Build.cs b/Source/SocketIOClient/SocketIOClient.Build.cs index 536a79d6..b5088dc3 100644 --- a/Source/SocketIOClient/SocketIOClient.Build.cs +++ b/Source/SocketIOClient/SocketIOClient.Build.cs @@ -67,7 +67,9 @@ public SocketIOClient(TargetInfo Target) new string[] { "Core", - "JSON", + "Json", + "JsonUtilities", + "SIOJson" // ... add other public dependencies that you statically link with here ... } ); @@ -79,7 +81,7 @@ public SocketIOClient(TargetInfo Target) "CoreUObject", "Engine", "Slate", - "SlateCore", + "SlateCore" // ... add private dependencies that you statically link with here ... } );