├── Source ├── ThirdParty │ └── .gitkeep ├── TurboLinkEditor │ ├── Public │ │ ├── TurboLinkEditorModule.h │ │ ├── GrpcMessageK2Node_MessageToJson.h │ │ └── GrpcMessageK2Node_JsonToMessage.h │ ├── TurboLinkEditor.Build.cs │ └── Private │ │ ├── SGrpcMessageTagGraphPin.h │ │ ├── TurboLinkEditorModule.cpp │ │ ├── SGrpcMessageTagGraphPin.cpp │ │ ├── GrpcMessageTagsManager.h │ │ ├── SGrpcMessageTagWidget.h │ │ ├── GrpcMessageTagsManager.cpp │ │ ├── GrpcMessageK2Node_MessageToJson.cpp │ │ ├── SGrpcMessageTagWidget.cpp │ │ └── GrpcMessageK2Node_JsonToMessage.cpp └── TurboLinkGrpc │ ├── Private │ ├── TurboLinkGrpcConfig.cpp │ ├── TurboLinkGrpcService.cpp │ ├── TurboLinkGrpcManager_Private.h │ ├── TurboLinkGrpcClient.cpp │ ├── TurboLinkGrpcContext.cpp │ ├── TurboLinkGrpcUtilities.cpp │ ├── TurboLinkGrpcManager_Private.cpp │ ├── TurboLinkGrpcModule.cpp │ ├── TurboLinkGrpcManager.cpp │ └── TurboLinkGrpcContext.h │ ├── Public │ ├── TurboLinkGrpcModule.h │ ├── TurboLinkGrpcService.h │ ├── TurboLinkGrpcManager.h │ ├── TurboLinkGrpcMessage.h │ ├── TurboLinkGrpcConfig.h │ ├── TurboLinkGrpcClient.h │ └── TurboLinkGrpcUtilities.h │ └── TurboLinkGrpc.Build.cs ├── Config └── FilterPlugin.ini ├── Resources └── Icon128.png ├── Tools ├── protoc-gen-turbolink.exe ├── fix_proto_h.txt ├── fix_proto_cpp.txt └── generate_code.cmd ├── .github ├── protos │ └── hello.proto └── workflows │ └── compile_proto.yml ├── TurboLink.uplugin ├── LICENSE ├── README_chs.md └── README.md /Source/ThirdParty/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Config/FilterPlugin.ini: -------------------------------------------------------------------------------- 1 | [FilterPlugin] 2 | /Tools/... 3 | -------------------------------------------------------------------------------- /Resources/Icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thejinchao/turbolink/HEAD/Resources/Icon128.png -------------------------------------------------------------------------------- /Tools/protoc-gen-turbolink.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thejinchao/turbolink/HEAD/Tools/protoc-gen-turbolink.exe -------------------------------------------------------------------------------- /.github/protos/hello.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package Greeter; 4 | option go_package = "./Greeter"; 5 | 6 | message HelloRequest { 7 | string name = 1; 8 | } 9 | message HelloResponse { 10 | string reply_message = 1; 11 | } 12 | service GreeterService { 13 | rpc Hello (HelloRequest) returns (HelloResponse); 14 | } -------------------------------------------------------------------------------- /Tools/fix_proto_h.txt: -------------------------------------------------------------------------------- 1 | #if defined(_MSC_VER) 2 | #pragma warning (disable:4946) // reinterpret_cast used between related classes: 'class1' and 'class2' 3 | #pragma warning (disable:4800) // 'type' : forcing value to bool 'true' or 'false' (performance warning) 4 | #pragma warning (disable:4582) // 'type': constructor is not implicitly called 5 | #endif 6 | -------------------------------------------------------------------------------- /Source/TurboLinkEditor/Public/TurboLinkEditorModule.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) Developed by Neo Jin. All Rights Reserved. 2 | #pragma once 3 | 4 | #include "CoreMinimal.h" 5 | #include "Modules/ModuleManager.h" 6 | 7 | class FTurboLinkEditorModule : public IModuleInterface 8 | { 9 | public: 10 | // IModuleInterface implementation 11 | virtual void StartupModule() override; 12 | virtual void ShutdownModule() override; 13 | }; 14 | -------------------------------------------------------------------------------- /Source/TurboLinkGrpc/Private/TurboLinkGrpcConfig.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) Developed by Neo Jin. All Rights Reserved. 2 | #include "TurboLinkGrpcConfig.h" 3 | 4 | UTurboLinkGrpcConfig::UTurboLinkGrpcConfig() 5 | { 6 | 7 | } 8 | 9 | FString UTurboLinkGrpcConfig::GetServiceEndPoint(const FString& ServiceName) const 10 | { 11 | const FString* serviceEndPoint = ServiceEndPoint.Find(ServiceName); 12 | if (serviceEndPoint) { 13 | return *serviceEndPoint; 14 | } 15 | return DefaultEndPoint; 16 | } 17 | 18 | FString UTurboLinkGrpcConfig::GetServerRootCerts() const 19 | { 20 | return ServerRootCerts.Replace(TEXT("\\n"), TEXT("\n")); 21 | } 22 | -------------------------------------------------------------------------------- /Source/TurboLinkEditor/TurboLinkEditor.Build.cs: -------------------------------------------------------------------------------- 1 | using UnrealBuildTool; 2 | 3 | public class TurboLinkEditor : ModuleRules 4 | { 5 | public TurboLinkEditor(ReadOnlyTargetRules Target) : base(Target) 6 | { 7 | PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; 8 | 9 | PublicDependencyModuleNames.AddRange( 10 | new string[] 11 | { 12 | "Core", 13 | "CoreUObject", 14 | "Slate", 15 | "Engine", 16 | "Slate", 17 | "SlateCore", 18 | "Settings", 19 | "UnrealEd", 20 | "KismetWidgets", 21 | "KismetCompiler", 22 | "BlueprintGraph", 23 | "GraphEditor", 24 | "Kismet", 25 | "PropertyEditor", 26 | "InputCore", 27 | "EditorStyle", 28 | "TurboLinkGrpc" 29 | } 30 | ); 31 | } 32 | } -------------------------------------------------------------------------------- /TurboLink.uplugin: -------------------------------------------------------------------------------- 1 | { 2 | "FileVersion": 3, 3 | "Version": 1, 4 | "VersionName": "1.4.2", 5 | "FriendlyName": "TurboLink", 6 | "Description": "Enables Google gRPC work with Unreal Engine using C++ and Blueprint", 7 | "Category": "Other", 8 | "CreatedBy": "Neo Jin", 9 | "CreatedByURL": "https://github.com/thejinchao/turbolink", 10 | "DocsURL": "https://github.com/thejinchao/turbolink/blob/main/README.md", 11 | "MarketplaceURL": "", 12 | "SupportURL": "", 13 | "CanContainContent": false, 14 | "Installed": false, 15 | "Modules": [ 16 | { 17 | "Name": "TurboLinkGrpc", 18 | "Type": "Runtime", 19 | "LoadingPhase": "PreDefault", 20 | "WhitelistPlatforms": [ 21 | "Win64", 22 | "Mac", 23 | "Linux", 24 | "IOS", 25 | "Android" 26 | ] 27 | }, 28 | { 29 | "Name": "TurboLinkEditor", 30 | "Type": "UncookedOnly", 31 | "LoadingPhase": "Default", 32 | "WhitelistPlatforms": [ 33 | "Win64", 34 | "Mac", 35 | "Linux" 36 | ] 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /Source/TurboLinkGrpc/Public/TurboLinkGrpcModule.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) Developed by Neo Jin. All Rights Reserved. 2 | #pragma once 3 | 4 | #include "CoreMinimal.h" 5 | #include "Modules/ModuleManager.h" 6 | #include "TurboLinkGrpcManager.h" 7 | #include "TurboLinkGrpcConfig.h" 8 | 9 | DECLARE_LOG_CATEGORY_EXTERN(LogTurboLink, Log, All); 10 | 11 | class TURBOLINKGRPC_API FTurboLinkGrpcModule : public IModuleInterface 12 | { 13 | friend class UTurboLinkGrpcManager; 14 | public: 15 | // IModuleInterface implementation 16 | virtual void StartupModule() override; 17 | virtual void ShutdownModule() override; 18 | 19 | const UTurboLinkGrpcConfig* GetTurboLinkGrpcConfig() const; 20 | 21 | #if WITH_EDITOR 22 | public: 23 | const TMap& GetMessageStructMap(); 24 | 25 | private: 26 | void RegisterAllGrpcMessageScriptStruct(); 27 | TMap GrpcMessageStructMap; 28 | #endif 29 | 30 | private: 31 | mutable const class UTurboLinkGrpcConfig* CachedSettings; 32 | 33 | public: 34 | FTurboLinkGrpcModule(); 35 | ~FTurboLinkGrpcModule(); 36 | }; 37 | -------------------------------------------------------------------------------- /.github/workflows/compile_proto.yml: -------------------------------------------------------------------------------- 1 | name: Generate code from proto 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | runs-on: windows-latest 9 | 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v2 13 | 14 | - name: Download and unzip third-party tools 15 | run: | 16 | curl -LO https://github.com/thejinchao/turbolink-libraries/releases/download/v1.3/protoc.zip 17 | tar -xvf protoc.zip -C Source\ThirdParty 18 | mkdir -p Tools\Output 19 | 20 | - name: Run generate_code.cmd 21 | working-directory: Tools\ 22 | # very important, since we use cmd scripts, the default is powershell - pwsh, 23 | # and a bug prevents errors to bubble and show up in the logs 24 | shell: cmd 25 | run: | 26 | copy ..\.github\protos\*.proto . 27 | for %%i in (*.proto) do (generate_code.cmd %%i Output) 28 | - name: Upload artifacts 29 | uses: actions/upload-artifact@v2 30 | with: 31 | name: Generated C++ code 32 | path: Tools\Output 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022-2023 thecodeway.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Source/TurboLinkGrpc/Private/TurboLinkGrpcService.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) Developed by Neo Jin. All Rights Reserved. 2 | #include "TurboLinkGrpcService.h" 3 | #include "TurboLinkGrpcUtilities.h" 4 | #include "TurboLinkGrpcManager_Private.h" 5 | #include "TurboLinkGrpcModule.h" 6 | 7 | UGrpcService::UGrpcService() 8 | { 9 | } 10 | 11 | UGrpcService::~UGrpcService() 12 | { 13 | } 14 | 15 | void UGrpcService::Tick(float DeltaTime) 16 | { 17 | for (auto it = ClientSet.CreateIterator(); it; ++it) 18 | { 19 | UGrpcClient* client = *it; 20 | 21 | client->Tick(DeltaTime); 22 | 23 | //All context has stoped 24 | if (client->bIsShutdowning && client->ContextMap.Num() == 0) 25 | { 26 | it.RemoveCurrent(); 27 | } 28 | } 29 | } 30 | 31 | void UGrpcService::Shutdown() 32 | { 33 | //already in shutdown progress? 34 | if (bIsShutingDown) return; 35 | 36 | OnServiceStateChanged.Clear(); 37 | 38 | //Disconnect all clients 39 | for (UGrpcClient* client : ClientSet) 40 | { 41 | client->Shutdown(); 42 | } 43 | bIsShutingDown = true; 44 | } 45 | 46 | void UGrpcService::RemoveClient(UGrpcClient* Client) 47 | { 48 | ClientSet.Remove(Client); 49 | } 50 | -------------------------------------------------------------------------------- /Source/TurboLinkGrpc/Private/TurboLinkGrpcManager_Private.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) Developed by Neo Jin. All Rights Reserved. 2 | #pragma once 3 | 4 | #include "TurboLinkGrpcManager.h" 5 | 6 | #include 7 | #include 8 | 9 | class UTurboLinkGrpcManager::Private 10 | { 11 | public: 12 | struct ServiceChannel 13 | { 14 | std::shared_ptr RpcChannel; 15 | grpc_connectivity_state ChannelState; 16 | std::string EndPoint; 17 | std::set AttachedServices; 18 | 19 | bool UpdateState() 20 | { 21 | grpc_connectivity_state newState = RpcChannel->GetState(false); 22 | if (newState == ChannelState) return false; 23 | 24 | ChannelState = newState; 25 | return true; 26 | } 27 | }; 28 | 29 | public: 30 | static EGrpcServiceState GrpcStateToServiceState(grpc_connectivity_state State); 31 | 32 | std::shared_ptr CreateServiceChannel(const char* EndPoint, UGrpcService* AttachedService); 33 | void RemoveServiceChannel(std::shared_ptr Channel, UGrpcService* AttachedService); 34 | 35 | static std::unique_ptr CreateRpcClientContext(); 36 | 37 | void ShutdownCompletionQueue(); 38 | 39 | public: 40 | std::map> ChannelMap; 41 | std::unique_ptr CompletionQueue; 42 | }; 43 | -------------------------------------------------------------------------------- /Tools/fix_proto_cpp.txt: -------------------------------------------------------------------------------- 1 | #if defined(_MSC_VER) 2 | #pragma warning (disable:4018) // 'expression' : signed/unsigned mismatch 3 | #pragma warning (disable:4065) // switch statement contains 'default' but no 'case' labels 4 | #pragma warning (disable:4146) // unary minus operator applied to unsigned type, result still unsigned 5 | #pragma warning (disable:4244) // 'conversion' conversion from 'type1' to 'type2', possible loss of data 6 | #pragma warning (disable:4251) // 'identifier' : class 'type' needs to have dll-interface to be used by clients of class 'type2' 7 | #pragma warning (disable:4267) // 'var' : conversion from 'size_t' to 'type', possible loss of data 8 | #pragma warning (disable:4305) // 'identifier' : truncation from 'type1' to 'type2' 9 | #pragma warning (disable:4307) // 'operator' : integral constant overflow 10 | #pragma warning (disable:4309) // 'conversion' : truncation of constant value 11 | #pragma warning (disable:4334) // 'operator' : result of 32-bit shift implicitly converted to 64 bits (was 64-bit shift intended?) 12 | #pragma warning (disable:4355) // 'this' : used in base member initializer list 13 | #pragma warning (disable:4506) // no definition for inline function 'function' 14 | #pragma warning (disable:4996) // The compiler encountered a deprecated declaration. 15 | #pragma warning (disable:4125) // decimal digit terminates octal escape sequence 16 | #endif 17 | -------------------------------------------------------------------------------- /Source/TurboLinkEditor/Private/SGrpcMessageTagGraphPin.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) Developed by Neo Jin. All Rights Reserved. 2 | #pragma once 3 | #include "CoreMinimal.h" 4 | #include "KismetPins/SGraphPinStructInstance.h" 5 | #include "Widgets/Views/STableViewBase.h" 6 | #include "Widgets/Views/STableRow.h" 7 | #include "SGrpcMessageTagWidget.h" 8 | 9 | /** Pin that represents a single gameplay tag, overrides the generic struct widget because tags have their own system for saving changes */ 10 | class SGrpcMessageTagGraphPin : public SGraphPinStructInstance 11 | { 12 | public: 13 | SLATE_BEGIN_ARGS(SGrpcMessageTagGraphPin) {} 14 | SLATE_END_ARGS() 15 | 16 | void Construct(const FArguments& InArgs, UEdGraphPin* InGraphPinObj); 17 | 18 | protected: 19 | //~ Begin SGraphPinStructInstance Interface 20 | virtual void ParseDefaultValueData() override; 21 | virtual void SaveDefaultValueData() override; 22 | virtual TSharedRef GetDefaultValueWidget() override; 23 | virtual TSharedRef GetEditContent() override; 24 | virtual TSharedRef GetDescriptionContent() override; 25 | //~ End SGraphPin Interface 26 | 27 | void OnGrpcMessageWidgetOpenStateChanged(bool bIsOpened); 28 | void OnMessageChecked(FName _SelectedMessageTag); 29 | 30 | // Holds the full tag name of urrent checked message 31 | FName SelectedMessageTagName; 32 | 33 | // The text block used to display the tag name of selected grpc message 34 | TSharedPtr TagTextBlock; 35 | // Current message tag widget 36 | TWeakPtr CurrentMessageTagWidget; 37 | }; 38 | -------------------------------------------------------------------------------- /Source/TurboLinkEditor/Private/TurboLinkEditorModule.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) Developed by Neo Jin. All Rights Reserved. 2 | #include "TurboLinkEditorModule.h" 3 | #include "GrpcMessageTagsManager.h" 4 | #include "EdGraphUtilities.h" 5 | #include "SGraphPin.h" 6 | #include "SGrpcMessageTagGraphPin.h" 7 | #include "EdGraphSchema_K2.h" 8 | 9 | class FGrpcMessageTagGraphPinFactory : public FGraphPanelPinFactory 10 | { 11 | virtual TSharedPtr CreatePin(class UEdGraphPin* InPin) const override 12 | { 13 | if (InPin->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct) 14 | { 15 | if (UScriptStruct* PinStructType = Cast(InPin->PinType.PinSubCategoryObject.Get())) 16 | { 17 | if (PinStructType->IsChildOf(FGrpcMessageTag::StaticStruct())) 18 | { 19 | return SNew(SGrpcMessageTagGraphPin, InPin); 20 | } 21 | } 22 | } 23 | 24 | return nullptr; 25 | } 26 | }; 27 | 28 | void FTurboLinkEditorModule::StartupModule() 29 | { 30 | // This will force initialization GrpcMessageTagsManager 31 | UGrpcMessageTagsManager::Get(); 32 | 33 | FCoreDelegates::OnPostEngineInit.AddLambda([]() { 34 | // Register GrpcMessageTagGraphPinFactory 35 | TSharedPtr grpcMessageTagGraphPinFactory = MakeShareable(new FGrpcMessageTagGraphPinFactory()); 36 | FEdGraphUtilities::RegisterVisualPinFactory(grpcMessageTagGraphPinFactory); 37 | }); 38 | } 39 | 40 | void FTurboLinkEditorModule::ShutdownModule() 41 | { 42 | UGrpcMessageTagsManager::Singleton = nullptr; 43 | } 44 | 45 | IMPLEMENT_MODULE(FTurboLinkEditorModule, TurboLinkEditor) -------------------------------------------------------------------------------- /Source/TurboLinkEditor/Public/GrpcMessageK2Node_MessageToJson.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) Developed by Neo Jin. All Rights Reserved. 2 | #pragma once 3 | #include "CoreMinimal.h" 4 | #include "K2Node.h" 5 | #include "GrpcMessageK2Node_MessageToJson.generated.h" 6 | 7 | UCLASS() 8 | class TURBOLINKEDITOR_API UGrpcMessageToJsonNode : public UK2Node 9 | { 10 | GENERATED_BODY() 11 | 12 | public: 13 | // Override UK2Node Interface 14 | virtual bool IsNodeSafeToIgnore() const override { return true; } 15 | 16 | // Override UEdGraphNode Interface. 17 | virtual void GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const override; 18 | virtual FText GetMenuCategory() const override; 19 | virtual FText GetTooltipText() const override; 20 | virtual FText GetNodeTitle(ENodeTitleType::Type Title) const override; 21 | virtual void AllocateDefaultPins() override; 22 | virtual void ExpandNode(FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph) override; 23 | virtual void PinDefaultValueChanged(UEdGraphPin* Pin) override; 24 | virtual void PinConnectionListChanged(UEdGraphPin* Pin); 25 | virtual void GetPinHoverText(const UEdGraphPin& Pin, FString& HoverTextOut) const override; 26 | 27 | // Get the then output pin 28 | UEdGraphPin* GetThenPin() const; 29 | // Get the input pin 30 | UEdGraphPin* GetInputPin(const FName& PinName) const; 31 | // Get the result output pin 32 | UEdGraphPin* GetResultPin() const; 33 | // Get linked struct script 34 | UScriptStruct* GetLinkedStruct(bool& bLinked) const; 35 | 36 | protected: 37 | // Refresh pins when input message was changed 38 | void OnMessagePinChanged(); 39 | 40 | protected: 41 | // Constructing FText strings can be costly, so we cache the node's title 42 | FNodeTextCache CachedNodeTitle; 43 | }; 44 | -------------------------------------------------------------------------------- /Source/TurboLinkGrpc/Public/TurboLinkGrpcService.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) Developed by Neo Jin. All Rights Reserved. 2 | #pragma once 3 | 4 | #include "CoreMinimal.h" 5 | #include "TurboLinkGrpcService.generated.h" 6 | 7 | class UTurboLinkGrpcManager; 8 | class UGrpcClient; 9 | 10 | UENUM(BlueprintType) 11 | enum class EGrpcServiceState : uint8 12 | { 13 | NotCreate = 0, 14 | 15 | Idle = 1, 16 | Connecting = 2, 17 | Ready = 3, 18 | TransientFailure = 4, 19 | Shutdown = 5, 20 | }; 21 | 22 | DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnServiceStateChanged, EGrpcServiceState, ServiceState); 23 | 24 | UCLASS(ClassGroup = TurboLink, BlueprintType, Abstract) 25 | class TURBOLINKGRPC_API UGrpcService : public UObject 26 | { 27 | GENERATED_BODY() 28 | friend class UTurboLinkGrpcManager; 29 | friend class UGrpcClient; 30 | 31 | public: 32 | virtual void Connect() { 33 | check(0 && "Must override this"); 34 | } 35 | 36 | virtual EGrpcServiceState GetServiceState() const { 37 | check(0 && "Must override this"); 38 | return EGrpcServiceState::NotCreate; 39 | } 40 | 41 | protected: 42 | virtual void Shutdown(); 43 | int32 RefrenceCounts = 0; 44 | double StartPendingShutdown = 0.0; 45 | bool bIsShutingDown = false; 46 | 47 | protected: 48 | template 49 | T* MakeClient() 50 | { 51 | auto client = NewObject(this); 52 | client->Service = this; 53 | 54 | ClientSet.Add(client); 55 | return client; 56 | } 57 | void Tick(float DeltaTime); 58 | 59 | public: 60 | UFUNCTION(BlueprintCallable, Category=TurboLink) 61 | void RemoveClient(UGrpcClient* Client); 62 | 63 | UPROPERTY() 64 | UTurboLinkGrpcManager* TurboLinkManager; 65 | 66 | UPROPERTY(BlueprintAssignable, Category = TurboLink) 67 | FOnServiceStateChanged OnServiceStateChanged; 68 | 69 | UPROPERTY() 70 | TSet ClientSet; 71 | 72 | public: 73 | UGrpcService(); 74 | virtual ~UGrpcService(); 75 | }; 76 | -------------------------------------------------------------------------------- /Source/TurboLinkEditor/Public/GrpcMessageK2Node_JsonToMessage.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) Developed by Neo Jin. All Rights Reserved. 2 | #pragma once 3 | #include "CoreMinimal.h" 4 | #include "K2Node.h" 5 | #include "GrpcMessageK2Node_JsonToMessage.generated.h" 6 | 7 | UCLASS() 8 | class TURBOLINKEDITOR_API UJsonToGrpcMessageNode : public UK2Node 9 | { 10 | GENERATED_BODY() 11 | 12 | public: 13 | // Override UK2Node Interface 14 | virtual bool IsNodeSafeToIgnore() const override { return true; } 15 | 16 | // Override UEdGraphNode Interface. 17 | virtual void GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const override; 18 | virtual FText GetMenuCategory() const override; 19 | virtual FText GetTooltipText() const override; 20 | virtual FText GetNodeTitle(ENodeTitleType::Type Title) const override; 21 | virtual void AllocateDefaultPins() override; 22 | virtual void PinDefaultValueChanged(UEdGraphPin* Pin) override; 23 | virtual void PinConnectionListChanged(UEdGraphPin* Pin); 24 | void ReallocatePinsDuringReconstruction(TArray& OldPins); 25 | virtual void ExpandNode(FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph) override; 26 | virtual void GetPinHoverText(const UEdGraphPin& Pin, FString& HoverTextOut) const override; 27 | 28 | // Get the then output pin 29 | UEdGraphPin* GetThenPin() const; 30 | // Get the input pin 31 | UEdGraphPin* GetInputPin(const FName& PinName, const TArray* InPinsToSearch=nullptr) const; 32 | // Get return message pin 33 | UEdGraphPin* GetReturnMessagePin(const TArray* InPinsToSearch=nullptr) const; 34 | // Get the result output pin 35 | UEdGraphPin* GetResultPin() const; 36 | 37 | protected: 38 | // Refresh pins when input message was changed 39 | void OnMessageTypeChanged(); 40 | // Get message script struct 41 | UScriptStruct* GetMessageScriptStruct(const TArray* InPinsToSearch=nullptr) const; 42 | }; 43 | -------------------------------------------------------------------------------- /Source/TurboLinkGrpc/Public/TurboLinkGrpcManager.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) Developed by Neo Jin. All Rights Reserved. 2 | #pragma once 3 | 4 | #include "CoreMinimal.h" 5 | #include "TurboLinkGrpcClient.h" 6 | #include "Subsystems/GameInstanceSubsystem.h" 7 | #include "Tickable.h" 8 | #include "TurboLinkGrpcManager.generated.h" 9 | 10 | class UGrpcService; 11 | class GrpcContext; 12 | 13 | UCLASS(ClassGroup=TurboLink, meta = (DisplayName = " TurboLink gRPC Manager")) 14 | class TURBOLINKGRPC_API UTurboLinkGrpcManager : public UGameInstanceSubsystem, public FTickableGameObject 15 | { 16 | GENERATED_BODY() 17 | 18 | public: 19 | //override function from UGameInstanceSubsystem 20 | virtual void Initialize(FSubsystemCollectionBase& Collection); 21 | virtual void Deinitialize(); 22 | 23 | //override from FTickableGameObject 24 | virtual bool IsTickable() const override { return bIsInitialized; } 25 | virtual void Tick(float DeltaTime) override; 26 | virtual TStatId GetStatId() const override { return GetStatID(); } 27 | 28 | void* GetNextTag(TSharedPtr Context); 29 | void RemoveTag(void* Tag); 30 | 31 | FGrpcContextHandle GetNextContextHandle(); 32 | public: 33 | class Private; 34 | Private* const d=nullptr; 35 | 36 | public: 37 | UFUNCTION(BlueprintCallable, Category = TurboLink) 38 | UGrpcService* MakeService(const FString& ServiceName); 39 | 40 | UFUNCTION(BlueprintCallable, Category = TurboLink) 41 | void ReleaseService(UGrpcService* Service); 42 | 43 | UFUNCTION(BlueprintCallable, Category = TurboLink) 44 | EGrpcServiceState GetServiceState(UGrpcService* Service); 45 | 46 | protected: 47 | UPROPERTY() 48 | TMap WorkingService; 49 | 50 | UPROPERTY() 51 | TSet ShutingDownService; 52 | 53 | private: 54 | TMap ServiceClassMap; 55 | 56 | TMap> GrpcContextMap; 57 | uint64_t NextTag = 0; 58 | 59 | uint32 NextContextHandle = 0; 60 | 61 | bool bIsInitialized = false; 62 | bool bIsShutdowning = false; 63 | 64 | public: 65 | UTurboLinkGrpcManager(); 66 | virtual ~UTurboLinkGrpcManager(); 67 | }; -------------------------------------------------------------------------------- /Source/TurboLinkGrpc/Private/TurboLinkGrpcClient.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) Developed by Neo Jin. All Rights Reserved. 2 | #include "TurboLinkGrpcClient.h" 3 | #include "TurboLinkGrpcContext.h" 4 | #include "TurboLinkGrpcModule.h" 5 | 6 | UGrpcClient::UGrpcClient() 7 | { 8 | UE_LOG(LogTurboLink, Log, TEXT("Construct GrpcClient")); 9 | } 10 | 11 | UGrpcClient::~UGrpcClient() 12 | { 13 | UE_LOG(LogTurboLink, Log, TEXT("Destruct GrpcClient")); 14 | } 15 | 16 | void UGrpcClient::AddContext(TSharedPtr Context) 17 | { 18 | ContextMap.Add(Context->GetHandle(), Context); 19 | } 20 | 21 | void UGrpcClient::RemoveContext(FGrpcContextHandle Handle) 22 | { 23 | ContextMap.Remove(Handle); 24 | } 25 | 26 | TSharedPtr* UGrpcClient::GetContext(FGrpcContextHandle Handle) 27 | { 28 | TSharedPtr* context = ContextMap.Find(Handle); 29 | return context; 30 | } 31 | 32 | EGrpcContextState UGrpcClient::GetContextState(FGrpcContextHandle Handle) const 33 | { 34 | const TSharedPtr* context = ContextMap.Find(Handle); 35 | if (context == nullptr) return EGrpcContextState::Done; 36 | 37 | return (*context)->GetState(); 38 | } 39 | 40 | void UGrpcClient::TryCancelContext(FGrpcContextHandle Handle) 41 | { 42 | const TSharedPtr* context = ContextMap.Find(Handle); 43 | if (context == nullptr) return; 44 | 45 | (*context)->TryCancel(); 46 | } 47 | 48 | void UGrpcClient::Tick(float DeltaTime) 49 | { 50 | //Remove all context that has done. 51 | if (ContextMap.Num() > 0) 52 | { 53 | TArray contextHandleArray; 54 | ContextMap.GetKeys(contextHandleArray); 55 | for (auto handle : contextHandleArray) 56 | { 57 | auto context = ContextMap[handle]; 58 | if (context->GetState() == EGrpcContextState::Done) 59 | { 60 | ContextMap.Remove(handle); 61 | } 62 | } 63 | ContextMap.Compact(); 64 | } 65 | } 66 | 67 | void UGrpcClient::Shutdown() 68 | { 69 | //already in shutdown progress? 70 | if (bIsShutdowning) return; 71 | 72 | OnContextStateChange.Clear(); 73 | //Close all context 74 | for (auto contextIter : ContextMap) 75 | { 76 | TSharedPtr context = contextIter.Value; 77 | if (context->GetState()== EGrpcContextState::Busy || context->GetState() == EGrpcContextState::Initialing) 78 | { 79 | context->TryCancel(); 80 | } 81 | } 82 | bIsShutdowning = true; 83 | } 84 | 85 | FString FGrpcResult::GetCodeString() const 86 | { 87 | int preFixLen = FString(TEXT("EGrpcResultCode::")).Len(); 88 | return UEnum::GetValueAsString(Code).RightChop(preFixLen); 89 | } 90 | 91 | FString FGrpcResult::GetMessageString() const 92 | { 93 | return FString::Printf(TEXT("%s:%s"), *GetCodeString(), *Message); 94 | } 95 | -------------------------------------------------------------------------------- /Tools/generate_code.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal enabledelayedexpansion 3 | 4 | ::set current directory as proto file path 5 | set INPUT_PROTO_PATH=%cd% 6 | 7 | ::make sure input proto files exist 8 | set INPUT_PROTO_FILE=%1 9 | if not exist %INPUT_PROTO_FILE% ( 10 | echo Input proto file '%INPUT_PROTO_FILE%' not exist! 11 | pause 12 | exit /b 1 13 | ) 14 | 15 | ::make sure output path exist 16 | set OUTPUT_PATH=%2 17 | if not exist %OUTPUT_PATH% ( 18 | echo Output path '%OUTPUT_PATH%' not exist! 19 | pause 20 | exit /b 1 21 | ) 22 | 23 | ::get turbolink plugin path 24 | pushd %~dp0\.. 25 | set TL_UE_PLUGIN_PATH=%cd% 26 | popd 27 | 28 | set TURBOLINK_PLUGIN_PATH=%TL_UE_PLUGIN_PATH%\Tools\protoc-gen-turbolink.exe 29 | set PROTOC_EXE_PATH=%TL_UE_PLUGIN_PATH%\Source\ThirdParty\protobuf\bin\protoc.exe 30 | set PROTOBUF_INC_PATH=%TL_UE_PLUGIN_PATH%\Source\ThirdParty\protobuf\include 31 | set GRPC_CPP_PLUGIN_EXE_PATH=%TL_UE_PLUGIN_PATH%\Source\ThirdParty\grpc\bin\grpc_cpp_plugin.exe 32 | set FIX_PROTO_CPP=%TL_UE_PLUGIN_PATH%\Tools\fix_proto_cpp.txt 33 | set FIX_PROTO_H=%TL_UE_PLUGIN_PATH%\Tools\fix_proto_h.txt 34 | set CPP_OUTPUT_PATH=%OUTPUT_PATH%\Private\pb 35 | if not exist %CPP_OUTPUT_PATH% mkdir %CPP_OUTPUT_PATH% 36 | 37 | :: Print Variables for debugging 38 | echo Input proto file: %INPUT_PROTO_FILE% 39 | echo Output path: %OUTPUT_PATH% 40 | 41 | echo TURBOLINK_PLUGIN_PATH=%TURBOLINK_PLUGIN_PATH% 42 | echo PROTOC_EXE_PATH=%PROTOC_EXE_PATH% 43 | echo PROTOBUF_INC_PATH=%PROTOBUF_INC_PATH% 44 | echo GRPC_CPP_PLUGIN_EXE_PATH=%GRPC_CPP_PLUGIN_EXE_PATH% 45 | echo FIX_PROTO_CPP=%FIX_PROTO_CPP% 46 | echo FIX_PROTO_H=%FIX_PROTO_H% 47 | echo CPP_OUTPUT_PATH=%CPP_OUTPUT_PATH% 48 | 49 | echo Checking existence of files or directories... 50 | 51 | set "variables=TURBOLINK_PLUGIN_PATH PROTOC_EXE_PATH PROTOBUF_INC_PATH GRPC_CPP_PLUGIN_EXE_PATH FIX_PROTO_CPP FIX_PROTO_H CPP_OUTPUT_PATH" 52 | 53 | set "errorFlag=0" 54 | for %%v in (%variables%) do ( 55 | set "currentVar=!%%v!" 56 | IF NOT EXIST "!currentVar!" ( 57 | echo !currentVar! not found at !currentVar! 58 | set "errorFlag=1" 59 | ) ELSE ( 60 | echo !currentVar! exists at !currentVar! 61 | ) 62 | ) 63 | 64 | if !errorFlag! equ 1 ( 65 | echo Error: At least one required file or directory not found. 66 | exit /b 1 67 | ) 68 | 69 | ::call protoc.exe 70 | "%PROTOC_EXE_PATH%" ^ 71 | --proto_path="%PROTOBUF_INC_PATH%" --proto_path="%INPUT_PROTO_PATH%" ^ 72 | --cpp_out="%CPP_OUTPUT_PATH%" ^ 73 | --plugin=protoc-gen-grpc="%GRPC_CPP_PLUGIN_EXE_PATH%" --grpc_out=%CPP_OUTPUT_PATH% ^ 74 | --plugin=protoc-gen-turbolink="%TURBOLINK_PLUGIN_PATH%" --turbolink_out="%OUTPUT_PATH%" ^ 75 | --turbolink_opt="GenerateJsonCode=true" ^ 76 | %INPUT_PROTO_FILE% 77 | 78 | :: fix protobuf compile warning 79 | call :FixCompileWarning "%FIX_PROTO_H%" %CPP_OUTPUT_PATH%\%INPUT_PROTO_FILE% "pb.h" 80 | call :FixCompileWarning "%FIX_PROTO_CPP%" %CPP_OUTPUT_PATH%\%INPUT_PROTO_FILE% "pb.cc" 81 | goto :eof 82 | 83 | :FixCompileWarning 84 | set FIX_FILE=%1 85 | set FILE_PATH=%~p2 86 | set FILE_NAME=%~n2.%~3 87 | 88 | pushd %FILE_PATH% 89 | copy /b %FIX_FILE%+%FILE_NAME% %FILE_NAME%.tmp 90 | del /f %FILE_NAME% 91 | rename %FILE_NAME%.tmp %FILE_NAME% 92 | popd 93 | 94 | goto :eof 95 | -------------------------------------------------------------------------------- /Source/TurboLinkGrpc/Public/TurboLinkGrpcMessage.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) Developed by Neo Jin. All Rights Reserved. 2 | #pragma once 3 | #include "CoreMinimal.h" 4 | #include "TurboLinkGrpcMessage.generated.h" 5 | 6 | #define DECLARE_JSON_FUNCTIONS() \ 7 | TURBOLINKGRPC_API virtual FString ToJsonString(bool bPrettyMode) const override; \ 8 | TURBOLINKGRPC_API virtual bool FromJsonString(const FString& JsonString) override; 9 | 10 | #define DEFINE_JSON_FUNCTIONS(StructName, GrpcStructName) \ 11 | FString StructName::ToJsonString(bool bPrettyMode) const \ 12 | { \ 13 | GrpcStructName message; \ 14 | TURBOLINK_TO_GRPC(this, &message); \ 15 | std::string json_string; \ 16 | ::google::protobuf::util::JsonPrintOptions options; \ 17 | options.add_whitespace=bPrettyMode; \ 18 | if(::google::protobuf::util::MessageToJsonString(message, &json_string, options).ok()) { \ 19 | return FString(StringCast((const UTF8CHAR*)json_string.c_str()).Get()); \ 20 | } \ 21 | return FString(TEXT("")); \ 22 | } \ 23 | bool StructName::FromJsonString(const FString& JsonString) \ 24 | { \ 25 | GrpcStructName grpcMessage; \ 26 | if(!::google::protobuf::util::JsonStringToMessage((const char*)StringCast(*JsonString).Get(), &grpcMessage).ok()) return false; \ 27 | GRPC_TO_TURBOLINK(&grpcMessage, this); \ 28 | return true; \ 29 | } 30 | 31 | USTRUCT(BlueprintType) 32 | struct FGrpcMessage 33 | { 34 | GENERATED_BODY() 35 | virtual ~FGrpcMessage() = default; 36 | 37 | virtual FString ToJsonString(bool bPrettyMode) const { return FString(TEXT("{}")); } 38 | virtual bool FromJsonString(const FString& JsonString) { return false; } 39 | }; 40 | 41 | USTRUCT(BlueprintType, meta = ( 42 | HasNativeMake = "/Script/TurboLinkGrpc.TurboLinkGrpcUtilities.MakeUInt64", 43 | HasNativeBreak = "/Script/TurboLinkGrpc.TurboLinkGrpcUtilities.BreakUInt64")) 44 | struct FUInt64 45 | { 46 | GENERATED_BODY() 47 | 48 | uint64 Value; 49 | 50 | operator uint64() const { return Value; } 51 | 52 | FUInt64() { this->Value = 0; } 53 | FUInt64(const uint64& _Value) { this->Value = _Value; } 54 | }; 55 | 56 | USTRUCT(BlueprintType, meta = ( 57 | HasNativeMake = "/Script/TurboLinkGrpc.TurboLinkGrpcUtilities.MakeDouble64", 58 | HasNativeBreak = "/Script/TurboLinkGrpc.TurboLinkGrpcUtilities.BreakDouble64")) 59 | struct FDouble64 60 | { 61 | GENERATED_BODY() 62 | 63 | double Value; 64 | 65 | operator double() const { return Value; } 66 | 67 | FDouble64() { this->Value = 0.0; } 68 | FDouble64(const double& _Value) { this->Value = _Value; } 69 | }; 70 | 71 | USTRUCT(BlueprintType, meta = ( 72 | HasNativeMake = "/Script/TurboLinkGrpc.TurboLinkGrpcUtilities.MakeUInt32", 73 | HasNativeBreak = "/Script/TurboLinkGrpc.TurboLinkGrpcUtilities.BreakUInt32")) 74 | struct FUInt32 75 | { 76 | GENERATED_BODY() 77 | 78 | uint32 Value; 79 | 80 | operator uint32() const { return Value; } 81 | 82 | FUInt32() { this->Value = 0; } 83 | FUInt32(const uint32& _Value) { this->Value = _Value; } 84 | }; 85 | 86 | USTRUCT(BlueprintType) 87 | struct FBytes 88 | { 89 | GENERATED_BODY() 90 | 91 | UPROPERTY(BlueprintReadWrite, Category = TurboLink) 92 | TArray Value; 93 | 94 | FBytes() { } 95 | FBytes(const uint8* _Value, int32 _Length) { Value.Append(_Value, _Length); } 96 | }; -------------------------------------------------------------------------------- /Source/TurboLinkGrpc/Public/TurboLinkGrpcConfig.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) Developed by Neo Jin. All Rights Reserved. 2 | #pragma once 3 | 4 | #include "CoreMinimal.h" 5 | #include "Engine/DeveloperSettings.h" 6 | #include "TurboLinkGrpcConfig.generated.h" 7 | 8 | UCLASS(config = Game, DefaultConfig) // DefaultConfig means 'Save object config only to Default INIs, never to local INIs.' 9 | class TURBOLINKGRPC_API UTurboLinkGrpcConfig : public UDeveloperSettings 10 | { 11 | GENERATED_BODY() 12 | 13 | public: 14 | //Enables retry functionality. 15 | //Defaults to true. When enabled, configurable retries are enabled when they are configured via the service config. 16 | UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category = "Channel Config") 17 | bool EnableRetries = true; 18 | 19 | // After a duration of this time the client/server pings its peer to see if the 20 | // transport is still alive.Int valued, milliseconds. 21 | UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category = "Channel Config") 22 | int KeepAliveTime=45000; 23 | 24 | // After waiting for a duration of this time, if the keepalive ping sender does 25 | // not receive the ping ack, it will close the transport.Int valued, milliseconds. 26 | UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category = "Channel Config") 27 | int KeepAliveTimeOut=30000; 28 | 29 | //How many pings can we send before needing to send a 30 | // data / header frame ? (0 indicates that an infinite number of 31 | // pings can be sent without sending a data frame or header frame) 32 | UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category = "Channel Config") 33 | int Http2MaxPingsWithoutData = 0; 34 | 35 | //Is it permissible to send keepalive pings without any outstanding streams. 36 | UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category = "Channel Config") 37 | bool KeepAlivePermitWithoutCalls = true; 38 | 39 | //Enable server-side tls connection type. Only sever needs to provide it's certificate to client. 40 | UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category = "Connection Config") 41 | bool EnableServerSideTLS = false; 42 | 43 | // The buffer containing the PEM encoding of the server root certificates. 44 | UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category = "Connection Config", 45 | DisplayName="PEM encoding server root certs(\\n to separate lines)", 46 | meta = (EditCondition = "EnableServerSideTLS", EditConditionHides)) 47 | FString ServerRootCerts; 48 | 49 | //Default grpc services endpoint. 50 | UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category = "Services Config") 51 | FString DefaultEndPoint; 52 | 53 | UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category = "Services Config") 54 | TMap ServiceEndPoint; 55 | 56 | //After waiting for a duration of this times(seconds), 57 | // the gRPC service object will be deleted if no other object references it. 58 | UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category = "Services Config") 59 | int KeepServiceAliveWithoutRefrenceSeconds = 60; 60 | 61 | public: 62 | UFUNCTION(BlueprintCallable, Category = TurboLink) 63 | FString GetServiceEndPoint(const FString& ServiceName) const; 64 | 65 | UFUNCTION(BlueprintCallable, Category = TurboLink) 66 | FString GetServerRootCerts() const; 67 | 68 | public: 69 | UTurboLinkGrpcConfig(); 70 | }; 71 | -------------------------------------------------------------------------------- /Source/TurboLinkGrpc/Public/TurboLinkGrpcClient.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) Developed by Neo Jin. All Rights Reserved. 2 | #pragma once 3 | 4 | #include "CoreMinimal.h" 5 | #include "TurboLinkGrpcService.h" 6 | #include "TurboLinkGrpcClient.generated.h" 7 | 8 | class UGrpcService; 9 | class GrpcContext; 10 | 11 | UENUM(BlueprintType) 12 | enum class EGrpcContextState : uint8 13 | { 14 | Ready = 0, 15 | Initialing = 1, 16 | Busy = 2, 17 | Done = 3 18 | }; 19 | 20 | UENUM(BlueprintType) 21 | enum class EGrpcResultCode : uint8 22 | { 23 | Ok = 0, 24 | Cancelled = 1, 25 | Unknown = 2, 26 | InvalidArgument = 3, 27 | DeadlineExceeded = 4, 28 | NotFound = 5, 29 | AlreadyExists = 6, 30 | PermissionDenied = 7, 31 | ResourceExhausted = 8, 32 | FailedPrecondition = 9, 33 | Aborted = 10, 34 | OutOfRange = 11, 35 | Unimplemented = 12, 36 | Internal = 13, 37 | Unavailable = 14, 38 | DataLoss = 15, 39 | Unauthenticated = 16, 40 | 41 | ConnectionFailed = 100, 42 | 43 | NotDefined = 0xFF, 44 | }; 45 | 46 | USTRUCT(BlueprintType) 47 | struct TURBOLINKGRPC_API FGrpcResult 48 | { 49 | GENERATED_BODY() 50 | 51 | UPROPERTY(BlueprintReadWrite, Category = TurboLink) 52 | EGrpcResultCode Code; 53 | 54 | UPROPERTY(BlueprintReadWrite, Category = TurboLink) 55 | FString Message; 56 | 57 | FString GetCodeString() const; 58 | FString GetMessageString() const; 59 | 60 | FGrpcResult() : Code(EGrpcResultCode::NotDefined), Message(TEXT("")) { } 61 | FGrpcResult(EGrpcResultCode _Code, const FString& _Message) : Code(_Code), Message(_Message) { } 62 | }; 63 | 64 | USTRUCT(BlueprintType) 65 | struct FGrpcContextHandle 66 | { 67 | GENERATED_BODY() 68 | 69 | uint32 Value; 70 | 71 | operator uint32() const { return Value; } 72 | FGrpcContextHandle() { this->Value = 0; } 73 | FGrpcContextHandle(const uint32& _Value) { this->Value = _Value; } 74 | }; 75 | 76 | FORCEINLINE uint32 GetTypeHash(const FGrpcContextHandle& GrpcContextHandle) 77 | { 78 | return GrpcContextHandle.Value; 79 | } 80 | 81 | USTRUCT(BlueprintType) 82 | struct TURBOLINKGRPC_API FGrpcMetaData 83 | { 84 | GENERATED_BODY() 85 | 86 | UPROPERTY(BlueprintReadWrite, Category = TurboLink) 87 | TMap MetaData; 88 | }; 89 | 90 | DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnContextStateChange, FGrpcContextHandle, Handle, EGrpcContextState, NewState); 91 | 92 | UCLASS(ClassGroup = TurboLink, BlueprintType, Abstract) 93 | class TURBOLINKGRPC_API UGrpcClient : public UObject 94 | { 95 | GENERATED_BODY() 96 | friend class UGrpcService; 97 | 98 | public: 99 | UPROPERTY(BlueprintAssignable, Category = TurboLink) 100 | FOnContextStateChange OnContextStateChange; 101 | 102 | UFUNCTION(BlueprintCallable, Category = TurboLink) 103 | EGrpcContextState GetContextState(FGrpcContextHandle Handle) const; 104 | 105 | UFUNCTION(BlueprintCallable, Category = TurboLink) 106 | void TryCancelContext(FGrpcContextHandle Handle); 107 | 108 | UFUNCTION(BlueprintCallable, Category = TurboLink) 109 | virtual void Shutdown(); 110 | 111 | protected: 112 | UGrpcService* Service; 113 | TMap> ContextMap; 114 | bool bIsShutdowning=false; 115 | 116 | template 117 | TSharedPtr MakeContext(FGrpcContextHandle handle) 118 | { 119 | auto context = MakeShared(handle, Service, this); 120 | AddContext(context); 121 | return context; 122 | } 123 | 124 | void AddContext(TSharedPtr Context); 125 | TSharedPtr* GetContext(FGrpcContextHandle Handle); 126 | void RemoveContext(FGrpcContextHandle Handle); 127 | 128 | void Tick(float DeltaTime); 129 | 130 | public: 131 | UGrpcClient(); 132 | virtual ~UGrpcClient(); 133 | }; 134 | -------------------------------------------------------------------------------- /Source/TurboLinkGrpc/Private/TurboLinkGrpcContext.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) Developed by Neo Jin. All Rights Reserved. 2 | #include "TurboLinkGrpcContext.h" 3 | #include "TurboLinkGrpcService.h" 4 | #include "TurboLinkGrpcManager.h" 5 | #include "TurboLinkGrpcModule.h" 6 | 7 | GrpcContext::GrpcContext(FGrpcContextHandle _Handle, UGrpcService* _Service, UGrpcClient* _Client) 8 | : Service(_Service) 9 | , Client(_Client) 10 | , Handle(_Handle) 11 | , ContextState(EGrpcContextState::Ready) 12 | { 13 | UE_LOG(LogTurboLink, Verbose, TEXT("Construct GrpcContext")); 14 | } 15 | 16 | GrpcContext::~GrpcContext() 17 | { 18 | UE_LOG(LogTurboLink, Verbose, TEXT("Destruct GrpcContext")); 19 | } 20 | 21 | void GrpcContext::UpdateState(EGrpcContextState NewState) 22 | { 23 | if (GetState() == NewState) return; 24 | 25 | ContextState = NewState; 26 | if (ContextState == EGrpcContextState::Initialing) 27 | { 28 | InitialTag = Service->TurboLinkManager->GetNextTag(AsShared()); 29 | WriteTag = Service->TurboLinkManager->GetNextTag(AsShared()); 30 | ReadTag = Service->TurboLinkManager->GetNextTag(AsShared()); 31 | } 32 | else if (ContextState == EGrpcContextState::Done) 33 | { 34 | Service->TurboLinkManager->RemoveTag(InitialTag); 35 | Service->TurboLinkManager->RemoveTag(WriteTag); 36 | Service->TurboLinkManager->RemoveTag(ReadTag); 37 | } 38 | 39 | if (Client->OnContextStateChange.IsBound()) 40 | { 41 | Client->OnContextStateChange.Broadcast(Handle, NewState); 42 | } 43 | } 44 | 45 | void GrpcContext::TryCancel() 46 | { 47 | if (GetState() == EGrpcContextState::Ready || GetState() == EGrpcContextState::Done) 48 | { 49 | return; 50 | } 51 | 52 | RpcContext->TryCancel(); 53 | } 54 | 55 | EGrpcResultCode GrpcContext::ConvertStatusCode(const grpc::Status& RpcStatus) 56 | { 57 | grpc::StatusCode errorCode = RpcStatus.error_code(); 58 | switch (errorCode) 59 | { 60 | case grpc::StatusCode::OK: 61 | return EGrpcResultCode::Ok; 62 | 63 | case grpc::StatusCode::CANCELLED: 64 | return EGrpcResultCode::Cancelled; 65 | 66 | case grpc::StatusCode::UNKNOWN: 67 | return EGrpcResultCode::Unknown; 68 | 69 | case grpc::StatusCode::INVALID_ARGUMENT: 70 | return EGrpcResultCode::InvalidArgument; 71 | 72 | case grpc::StatusCode::DEADLINE_EXCEEDED: 73 | return EGrpcResultCode::DeadlineExceeded; 74 | 75 | case grpc::StatusCode::NOT_FOUND: 76 | return EGrpcResultCode::NotFound; 77 | 78 | case grpc::StatusCode::ALREADY_EXISTS: 79 | return EGrpcResultCode::AlreadyExists; 80 | 81 | case grpc::StatusCode::PERMISSION_DENIED: 82 | return EGrpcResultCode::PermissionDenied; 83 | 84 | case grpc::StatusCode::UNAUTHENTICATED: 85 | return EGrpcResultCode::Unauthenticated; 86 | 87 | case grpc::StatusCode::RESOURCE_EXHAUSTED: 88 | return EGrpcResultCode::ResourceExhausted; 89 | 90 | case grpc::StatusCode::FAILED_PRECONDITION: 91 | return EGrpcResultCode::FailedPrecondition; 92 | 93 | case grpc::StatusCode::ABORTED: 94 | return EGrpcResultCode::Aborted; 95 | 96 | case grpc::StatusCode::OUT_OF_RANGE: 97 | return EGrpcResultCode::OutOfRange; 98 | 99 | case grpc::StatusCode::UNIMPLEMENTED: 100 | return EGrpcResultCode::Unimplemented; 101 | 102 | case grpc::StatusCode::INTERNAL: 103 | return EGrpcResultCode::Internal; 104 | 105 | case grpc::StatusCode::UNAVAILABLE: 106 | return EGrpcResultCode::Unavailable; 107 | 108 | case grpc::StatusCode::DATA_LOSS: 109 | return EGrpcResultCode::DataLoss; 110 | 111 | default: 112 | return EGrpcResultCode::NotDefined; 113 | } 114 | } 115 | 116 | FGrpcResult GrpcContext::MakeGrpcResult(const grpc::Status& RpcStatus) 117 | { 118 | EGrpcResultCode errorCode = ConvertStatusCode(RpcStatus); 119 | FString message = StringCast((const UTF8CHAR*)RpcStatus.error_message().c_str()).Get(); 120 | 121 | return FGrpcResult(errorCode, message); 122 | } -------------------------------------------------------------------------------- /Source/TurboLinkGrpc/Private/TurboLinkGrpcUtilities.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) Developed by Neo Jin. All Rights Reserved. 2 | #include "TurboLinkGrpcUtilities.h" 3 | #include "TurboLinkGrpcModule.h" 4 | #include "Kismet/GameplayStatics.h" 5 | #include "Engine/Engine.h" 6 | #include "Engine/GameInstance.h" 7 | 8 | UTurboLinkGrpcManager* UTurboLinkGrpcUtilities::GetTurboLinkGrpcManager(UObject* WorldContextObject) 9 | { 10 | if (WorldContextObject == nullptr) 11 | { 12 | TIndirectArray Worlds = GEngine->GetWorldContexts(); 13 | for (auto World : Worlds) 14 | { 15 | if (World.WorldType == EWorldType::Game || World.WorldType == EWorldType::PIE) 16 | { 17 | WorldContextObject = World.World(); 18 | break; 19 | } 20 | } 21 | } 22 | if (WorldContextObject == nullptr) return nullptr; 23 | 24 | UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(WorldContextObject); 25 | if (GameInstance == nullptr) return nullptr; 26 | 27 | return GameInstance->GetSubsystem(); 28 | } 29 | 30 | const UTurboLinkGrpcConfig* UTurboLinkGrpcUtilities::GetTurboLinkGrpcConfig() 31 | { 32 | FTurboLinkGrpcModule* turboLinkModule = FModuleManager::GetModulePtr("TurboLinkGrpc"); 33 | if (turboLinkModule == nullptr) 34 | { 35 | turboLinkModule = &(FModuleManager::LoadModuleChecked("TurboLinkGrpc")); 36 | } 37 | 38 | return turboLinkModule->GetTurboLinkGrpcConfig(); 39 | } 40 | 41 | FString UGrpcMessageToJsonFunctionLibrary::GrpcMessageToJsonInternal(UStruct* GrpcMessage, bool bPrettyMode) 42 | { 43 | checkf(false, TEXT("This function should not be called!")); 44 | return FString(TEXT("")); 45 | } 46 | 47 | DEFINE_FUNCTION(UGrpcMessageToJsonFunctionLibrary::execGrpcMessageToJsonInternal) 48 | { 49 | Stack.MostRecentPropertyAddress = nullptr; 50 | Stack.MostRecentProperty = nullptr; 51 | Stack.StepCompiledIn(nullptr); 52 | void* StructPtr = Stack.MostRecentPropertyAddress; 53 | FStructProperty* StructProperty = CastField(Stack.MostRecentProperty); 54 | 55 | P_GET_UBOOL(PrettyMode); 56 | 57 | P_FINISH; 58 | P_NATIVE_BEGIN; 59 | *((FString*)RESULT_PARAM) = GrpcMessageToJson_Impl(StructPtr, StructProperty, PrettyMode); 60 | P_NATIVE_END; 61 | } 62 | 63 | FString UGrpcMessageToJsonFunctionLibrary::GrpcMessageToJson_Impl(void* StructPtr, FStructProperty* StructProperty, bool bPrettyMode) 64 | { 65 | bool bIsGrpcMessage = StructProperty->Struct->IsChildOf(FGrpcMessage::StaticStruct()); 66 | if (!bIsGrpcMessage) 67 | { 68 | return FString(TEXT("")); 69 | } 70 | return StaticCast(StructPtr)->ToJsonString(bPrettyMode); 71 | } 72 | 73 | bool UGrpcMessageToJsonFunctionLibrary::JsonToGrpcMessageInternal(const FString& JsonString, UStruct*& ReturnMessage) 74 | { 75 | checkf(false, TEXT("This function should not be called!")); 76 | return false; 77 | } 78 | 79 | DEFINE_FUNCTION(UGrpcMessageToJsonFunctionLibrary::execJsonToGrpcMessageInternal) 80 | { 81 | P_GET_PROPERTY(FStrProperty, JsonString); 82 | 83 | Stack.StepCompiledIn(NULL); 84 | FStructProperty* StructProperty = ExactCastField(Stack.MostRecentProperty); 85 | void* StructPtr = Stack.MostRecentPropertyAddress; 86 | 87 | P_FINISH; 88 | P_NATIVE_BEGIN; 89 | *(bool*)RESULT_PARAM = JsonToGrpcMessage_Impl(JsonString, StructPtr, StructProperty); 90 | P_NATIVE_END; 91 | } 92 | 93 | bool UGrpcMessageToJsonFunctionLibrary::JsonToGrpcMessage_Impl(const FString& JsonString, void* StructPtr, FStructProperty* StructProperty) 94 | { 95 | UScriptStruct* scriptStruct = StructProperty->Struct; 96 | bool bIsGrpcMessage = scriptStruct->IsChildOf(FGrpcMessage::StaticStruct()); 97 | if (!bIsGrpcMessage) return false; 98 | 99 | return StaticCast(StructPtr)->FromJsonString(JsonString); 100 | } 101 | -------------------------------------------------------------------------------- /Source/TurboLinkEditor/Private/SGrpcMessageTagGraphPin.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) Developed by Neo Jin. All Rights Reserved. 2 | #include "SGrpcMessageTagGraphPin.h" 3 | #include "SGrpcMessageTagWidget.h" 4 | 5 | #define LOCTEXT_NAMESPACE "GrpcMessageTagGraphPin" 6 | 7 | void SGrpcMessageTagGraphPin::Construct( const FArguments& InArgs, UEdGraphPin* InGraphPinObj ) 8 | { 9 | SGraphPin::Construct( SGraphPin::FArguments(), InGraphPinObj ); 10 | } 11 | 12 | void SGrpcMessageTagGraphPin::ParseDefaultValueData() 13 | { 14 | // Read using import text, but with serialize flag set so it doesn't always throw away invalid ones 15 | FGrpcMessageTag grpcMessageTag; 16 | grpcMessageTag.FromExportString(GraphPinObj->GetDefaultAsString(), PPF_SerializedAsImportText); 17 | 18 | SelectedMessageTagName = grpcMessageTag.TagName; 19 | } 20 | 21 | 22 | TSharedRef SGrpcMessageTagGraphPin::GetDefaultValueWidget() 23 | { 24 | if (GraphPinObj == nullptr) 25 | { 26 | return SNullWidget::NullWidget; 27 | } 28 | 29 | ParseDefaultValueData(); 30 | 31 | //Create widget 32 | return SNew(SVerticalBox) 33 | .Visibility(this, &SGraphPin::GetDefaultValueVisibility) 34 | + SVerticalBox::Slot() 35 | .AutoHeight() 36 | [ 37 | SAssignNew(ComboButton, SComboButton) 38 | .OnGetMenuContent(this, &SGrpcMessageTagGraphPin::GetEditContent) 39 | .OnMenuOpenChanged(this, &SGrpcMessageTagGraphPin::OnGrpcMessageWidgetOpenStateChanged) 40 | .ContentPadding(FMargin(2.0f, 2.0f)) 41 | .ButtonContent() 42 | [ 43 | SNew(STextBlock) 44 | .Text(LOCTEXT("SelectText", "Select Message Type")) 45 | ] 46 | ] 47 | + SVerticalBox::Slot() 48 | .AutoHeight() 49 | [ 50 | GetDescriptionContent() 51 | ]; 52 | } 53 | 54 | TSharedRef SGrpcMessageTagGraphPin::GetEditContent() 55 | { 56 | TSharedRef messageTagWidget = 57 | SNew(SGrpcMessageTagWidget) 58 | .OnTagChanged(this, &SGrpcMessageTagGraphPin::OnMessageChecked) 59 | .SelectedMessageTagName(SelectedMessageTagName) 60 | .Visibility(this, &SGraphPin::GetDefaultValueVisibility); 61 | 62 | CurrentMessageTagWidget = messageTagWidget; 63 | 64 | return SNew( SVerticalBox ) 65 | +SVerticalBox::Slot() 66 | .AutoHeight() 67 | .MaxHeight( 400 ) 68 | [ 69 | messageTagWidget 70 | ]; 71 | 72 | } 73 | 74 | TSharedRef SGrpcMessageTagGraphPin::GetDescriptionContent() 75 | { 76 | SAssignNew(TagTextBlock, STextBlock) 77 | .Text(FText::FromName(SelectedMessageTagName)) 78 | .Visibility(EVisibility::Visible); 79 | 80 | return TagTextBlock->AsShared(); 81 | } 82 | 83 | void SGrpcMessageTagGraphPin::OnGrpcMessageWidgetOpenStateChanged(bool bIsOpened) 84 | { 85 | if (bIsOpened) 86 | { 87 | //set edit focus to search box 88 | TSharedPtr currentMessageTagWidget = CurrentMessageTagWidget.Pin(); 89 | if (currentMessageTagWidget.IsValid()) 90 | { 91 | ComboButton->SetMenuContentWidgetToFocus(currentMessageTagWidget->GetWidgetToFocusOnOpen()); 92 | } 93 | } 94 | } 95 | 96 | void SGrpcMessageTagGraphPin::OnMessageChecked(FName _SelectedMessageTag) 97 | { 98 | SelectedMessageTagName = _SelectedMessageTag; 99 | SaveDefaultValueData(); 100 | } 101 | 102 | void SGrpcMessageTagGraphPin::SaveDefaultValueData() 103 | { 104 | // Refresh selected message tag name 105 | if (TagTextBlock.IsValid()) 106 | { 107 | TagTextBlock->SetText(FText::FromName(SelectedMessageTagName)); 108 | } 109 | 110 | // Set serialize string to pin default value 111 | FString SerializeString = FString::Printf(TEXT("(TagName=\"%s\")"), *SelectedMessageTagName.ToString()); 112 | FString CurrentDefaultValue = GraphPinObj->GetDefaultAsString(); 113 | if (CurrentDefaultValue.IsEmpty() || CurrentDefaultValue == TEXT("(TagName=\"\")")) 114 | { 115 | CurrentDefaultValue = FString(TEXT("")); 116 | } 117 | if (!CurrentDefaultValue.Equals(SerializeString)) 118 | { 119 | GraphPinObj->GetSchema()->TrySetDefaultValue(*GraphPinObj, SerializeString); 120 | } 121 | } 122 | 123 | #undef LOCTEXT_NAMESPACE 124 | -------------------------------------------------------------------------------- /Source/TurboLinkEditor/Private/GrpcMessageTagsManager.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) Developed by Neo Jin. All Rights Reserved. 2 | #pragma once 3 | #include "CoreMinimal.h" 4 | #include "GrpcMessageTagsManager.generated.h" 5 | 6 | USTRUCT() 7 | struct FGrpcMessageTag 8 | { 9 | GENERATED_BODY() 10 | FGrpcMessageTag() {}; 11 | 12 | // Simple constructor, passing redundant data for performance */ 13 | FGrpcMessageTag(FName _SimpleTagName, FName _TagName, TSharedPtr _ParentNode, UScriptStruct* _MessageScriptStruct); 14 | 15 | // Get the children nodes of this node 16 | FORCEINLINE TArray< TSharedPtr >& GetChildTagNodes() { return ChildTags; } 17 | // Get the parent tag node of this node 18 | FORCEINLINE TSharedPtr GetParentTagNode() const { return ParentNode; } 19 | // Reset the node of all of its values 20 | void ResetNode(); 21 | 22 | // Raw name for this tag at current rank in the tree 23 | FName SimpleTagName; 24 | 25 | // Full tag name 26 | UPROPERTY(VisibleAnywhere, SaveGame, Category = TurboLink) 27 | FName TagName; 28 | 29 | // Child gameplay tag nodes 30 | TArray> ChildTags; 31 | // Owner gameplay tag node, if any 32 | TSharedPtr ParentNode; 33 | // The ScriptStruct of grpc message 34 | UScriptStruct* MessageScriptStruct; 35 | 36 | // Operators 37 | FORCEINLINE bool operator==(FGrpcMessageTag const& Other) const 38 | { 39 | return TagName == Other.TagName; 40 | } 41 | 42 | FORCEINLINE bool operator!=(FGrpcMessageTag const& Other) const 43 | { 44 | return TagName != Other.TagName; 45 | } 46 | 47 | FORCEINLINE bool operator<(FGrpcMessageTag const& Other) const 48 | { 49 | return TagName.LexicalLess(Other.TagName); 50 | } 51 | 52 | friend void operator<<(FStructuredArchive::FSlot Slot, FGrpcMessageTag& GrpcMessageTag) 53 | { 54 | Slot << GrpcMessageTag.TagName; 55 | } 56 | // Sets from a ImportText string, used in asset registry 57 | void FromExportString(const FString& ExportString, int32 PortFlags = 0); 58 | }; 59 | 60 | UCLASS() 61 | class UGrpcMessageTagsManager : public UObject 62 | { 63 | GENERATED_UCLASS_BODY() 64 | 65 | public: 66 | void AddGrpcMessageTag(const FName& OriginalTagName, UScriptStruct* MessageScriptStruct); 67 | // Gets a copy of the GrpcMessageRootTag array 68 | void GetGrpcMessageRootTags(TArray< TSharedPtr >& OutTagArray) const; 69 | // find grpc message tag from name 70 | TSharedPtr FindGrpcMessageTag(const FName& TagName, bool bWithScriptStructOnly); 71 | 72 | private: 73 | // Roots of gameplay tag nodes 74 | TSharedPtr GrpcMessageRootTag; 75 | bool bIsConstructingTagTree = false; 76 | 77 | // Construct the grpc message tag tree 78 | void ConstructTagTree(); 79 | // Destroy the grpc message tag tree 80 | void DestroyTagTree(); 81 | /** 82 | * Helper function to insert a tag into a tag node array 83 | * 84 | * @param Tag Short name of tag to insert 85 | * @param FullTag Full tag, passed in for performance 86 | * @param ParentNode Parent node, if any, for the tag 87 | * @param NodeArray Node array to insert the new node into, if necessary (if the tag already exists, no insertion will occur) 88 | * @param MessageScriptStruct UScriptStruct for message struct 89 | * 90 | * @return Index of the node of the tag 91 | */ 92 | int32 InsertTagIntoNodeArray(FName Tag, FName FullTag, 93 | TSharedPtr ParentNode, TArray< TSharedPtr >& NodeArray, UScriptStruct* MessageScriptStruct); 94 | 95 | TSharedPtr FindGrpcMessageTagInArray(const TArray>& grpcMessageTags, 96 | const FName& TagName, bool bWithScriptStructOnly); 97 | public: 98 | ~UGrpcMessageTagsManager(); 99 | // The Manager singleton 100 | static UGrpcMessageTagsManager* Singleton; 101 | // Initializes the singleton manager 102 | static void InitializeManager(); 103 | // Returns the global UGrpcMessageTagsManager manager 104 | static UGrpcMessageTagsManager& Get() 105 | { 106 | if (Singleton == nullptr) 107 | { 108 | InitializeManager(); 109 | } 110 | return *Singleton; 111 | } 112 | }; 113 | -------------------------------------------------------------------------------- /Source/TurboLinkGrpc/Private/TurboLinkGrpcManager_Private.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) Developed by Neo Jin. All Rights Reserved. 2 | #include "TurboLinkGrpcManager_Private.h" 3 | #include "TurboLinkGrpcModule.h" 4 | 5 | EGrpcServiceState UTurboLinkGrpcManager::Private::GrpcStateToServiceState(grpc_connectivity_state State) 6 | { 7 | switch (State) 8 | { 9 | case GRPC_CHANNEL_IDLE: 10 | return EGrpcServiceState::Idle; 11 | 12 | case GRPC_CHANNEL_CONNECTING: 13 | return EGrpcServiceState::Connecting; 14 | 15 | case GRPC_CHANNEL_READY: 16 | return EGrpcServiceState::Ready; 17 | 18 | case GRPC_CHANNEL_TRANSIENT_FAILURE: 19 | return EGrpcServiceState::TransientFailure; 20 | 21 | case GRPC_CHANNEL_SHUTDOWN: 22 | return EGrpcServiceState::Shutdown; 23 | } 24 | 25 | return EGrpcServiceState::NotCreate; 26 | } 27 | 28 | std::shared_ptr UTurboLinkGrpcManager::Private::CreateServiceChannel(const char* EndPoint, UGrpcService* AttachedService) 29 | { 30 | auto itServiceChannel = ChannelMap.find(EndPoint); 31 | if (itServiceChannel != ChannelMap.end()) 32 | { 33 | itServiceChannel->second->AttachedServices.insert(AttachedService); 34 | return itServiceChannel->second; 35 | } 36 | 37 | std::shared_ptr channel = std::make_shared(); 38 | 39 | //get config instance 40 | FTurboLinkGrpcModule* turboLinkModule = FModuleManager::GetModulePtr("TurboLinkGrpc"); 41 | const UTurboLinkGrpcConfig* config = turboLinkModule->GetTurboLinkGrpcConfig(); 42 | 43 | //apply channel arguments 44 | grpc::ChannelArguments args; 45 | args.SetInt(GRPC_ARG_ENABLE_RETRIES, config->EnableRetries ? 1: 0); 46 | args.SetInt(GRPC_ARG_KEEPALIVE_TIME_MS, config->KeepAliveTime); 47 | args.SetInt(GRPC_ARG_KEEPALIVE_TIMEOUT_MS, config->KeepAliveTimeOut); 48 | args.SetInt(GRPC_ARG_KEEPALIVE_PERMIT_WITHOUT_CALLS, config->KeepAlivePermitWithoutCalls ? 1 : 0); 49 | args.SetInt(GRPC_ARG_HTTP2_MAX_PINGS_WITHOUT_DATA, config->Http2MaxPingsWithoutData); 50 | 51 | //is server-side tls mode? 52 | if (config->EnableServerSideTLS) 53 | { 54 | grpc::SslCredentialsOptions ssl_opts; 55 | ssl_opts.pem_root_certs = (const char*)StringCast(*(config->GetServerRootCerts())).Get(); 56 | channel->RpcChannel = grpc::CreateCustomChannel(EndPoint, grpc::SslCredentials(ssl_opts), args); 57 | } 58 | else 59 | { 60 | //Insecure mode 61 | channel->RpcChannel = grpc::CreateCustomChannel(EndPoint, grpc::InsecureChannelCredentials(), args); 62 | } 63 | 64 | channel->EndPoint = EndPoint; 65 | channel->AttachedServices.insert(AttachedService); 66 | channel->ChannelState = channel->RpcChannel->GetState(false); 67 | ChannelMap.insert(std::make_pair(EndPoint, channel)); 68 | return channel; 69 | } 70 | 71 | void UTurboLinkGrpcManager::Private::RemoveServiceChannel(std::shared_ptr Channel, UGrpcService* AttachedService) 72 | { 73 | auto itServiceChannel = ChannelMap.find(Channel->EndPoint); 74 | if (itServiceChannel == ChannelMap.end() || itServiceChannel->second != Channel) 75 | { 76 | return; 77 | } 78 | //Detach GrpcService 79 | Channel->AttachedServices.erase(AttachedService); 80 | 81 | //Still another GrpcService working 82 | if (!(Channel->AttachedServices.empty())) return; 83 | 84 | //Empty Channel, shutdown 85 | Channel->RpcChannel = nullptr; 86 | ChannelMap.erase(Channel->EndPoint); 87 | } 88 | 89 | std::unique_ptr UTurboLinkGrpcManager::Private::CreateRpcClientContext(void) 90 | { 91 | std::unique_ptr context = std::make_unique(); 92 | return context; 93 | } 94 | 95 | void UTurboLinkGrpcManager::Private::ShutdownCompletionQueue() 96 | { 97 | if (CompletionQueue == nullptr) return; 98 | 99 | //Shutdown! 100 | CompletionQueue->Shutdown(); 101 | 102 | //Remove all event 103 | void* got_tag; 104 | bool ok; 105 | 106 | gpr_timespec deadline; 107 | deadline.clock_type = GPR_CLOCK_MONOTONIC; 108 | deadline.tv_nsec = 1; 109 | deadline.tv_sec = 0; 110 | auto result = CompletionQueue->AsyncNext(&got_tag, &ok, deadline); 111 | 112 | while (result != grpc::CompletionQueue::NextStatus::SHUTDOWN) 113 | { 114 | result = CompletionQueue->AsyncNext(&got_tag, &ok, deadline); 115 | } 116 | } 117 | 118 | -------------------------------------------------------------------------------- /Source/TurboLinkGrpc/Private/TurboLinkGrpcModule.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) Developed by Neo Jin. All Rights Reserved. 2 | #include "TurboLinkGrpcModule.h" 3 | #include "TurboLinkGrpcManager.h" 4 | 5 | #include "UObject/UObjectGlobals.h" 6 | #include "Misc/ConfigCacheIni.h" 7 | #include "Misc/Char.h" 8 | #include "HAL/ExceptionHandling.h" 9 | #include "Logging/LogMacros.h" 10 | #include "Logging/LogVerbosity.h" 11 | 12 | #if PLATFORM_WINDOWS 13 | #include "Windows/PreWindowsApi.h" 14 | #endif 15 | 16 | #include "grpc/grpc.h" 17 | #include "grpc/support/log.h" 18 | #include "google/protobuf/descriptor_database.h" 19 | 20 | #if PLATFORM_WINDOWS 21 | #include "Windows/PostWindowsApi.h" 22 | #endif 23 | 24 | #define LOCTEXT_NAMESPACE "FTurboLinkGrpcModule" 25 | 26 | DEFINE_LOG_CATEGORY(LogTurboLink); 27 | 28 | void GrpcLogEntry(gpr_log_func_args* args) 29 | { 30 | FString logMessage = FString::Printf(TEXT("grpc:(%s:%d)%s"), 31 | StringCast((const UTF8CHAR*)(args->file)).Get(), 32 | args->line, 33 | StringCast((const UTF8CHAR*)(args->message)).Get()); 34 | 35 | ELogVerbosity::Type logSeverity = ELogVerbosity::Type::Log; 36 | switch (args->severity) 37 | { 38 | case GPR_LOG_SEVERITY_DEBUG: 39 | UE_LOG(LogTurboLink, Verbose, TEXT("%s"), *logMessage); 40 | break; 41 | case GPR_LOG_SEVERITY_INFO: 42 | UE_LOG(LogTurboLink, Log, TEXT("%s"), *logMessage); 43 | break; 44 | case GPR_LOG_SEVERITY_ERROR: 45 | UE_LOG(LogTurboLink, Error, TEXT("%s"), *logMessage); 46 | break; 47 | } 48 | 49 | } 50 | 51 | FTurboLinkGrpcModule::FTurboLinkGrpcModule() 52 | : CachedSettings(nullptr) 53 | { 54 | } 55 | 56 | FTurboLinkGrpcModule::~FTurboLinkGrpcModule() 57 | { 58 | } 59 | 60 | void FTurboLinkGrpcModule::StartupModule() 61 | { 62 | UE_LOG(LogTurboLink, Log, TEXT("Startup TurboLinkGrpcModule")); 63 | 64 | gpr_set_log_verbosity(GPR_LOG_SEVERITY_DEBUG); 65 | gpr_set_log_function(GrpcLogEntry); 66 | 67 | //Access config via 'class default object' 68 | GetTurboLinkGrpcConfig(); 69 | 70 | #if WITH_EDITOR 71 | //register grpc message script struct 72 | this->RegisterAllGrpcMessageScriptStruct(); 73 | #endif 74 | } 75 | 76 | #if WITH_EDITOR 77 | const TMap& FTurboLinkGrpcModule::GetMessageStructMap() 78 | { 79 | return GrpcMessageStructMap; 80 | } 81 | 82 | void FTurboLinkGrpcModule::RegisterAllGrpcMessageScriptStruct() 83 | { 84 | google::protobuf::DescriptorDatabase* protobufDataBase = google::protobuf::DescriptorPool::internal_generated_database(); 85 | if (protobufDataBase == nullptr) return; 86 | 87 | std::vector msgNames; 88 | protobufDataBase->FindAllMessageNames(&msgNames); 89 | for (size_t i = 0; i < msgNames.size(); i++) 90 | { 91 | FString rpcMessageName(StringCast((const UTF8CHAR*)msgNames[i].c_str()).Get()); 92 | 93 | TArray subNames; 94 | rpcMessageName.ParseIntoArray(subNames, TEXT("."), true); 95 | 96 | FString grpcMessageName = "Grpc"; 97 | for (int32 index = 0; index < subNames.Num(); index++) 98 | { 99 | FString subName = subNames[index]; 100 | grpcMessageName += TChar::ToUpper(subName[0]); 101 | grpcMessageName += subName.RightChop(1); 102 | } 103 | 104 | //add message script struct to map 105 | #if (ENGINE_MAJOR_VERSION==5 && ENGINE_MINOR_VERSION>=1) || ENGINE_MAJOR_VERSION>5 106 | UScriptStruct* scriptStruct = FindFirstObject(*grpcMessageName); 107 | #else 108 | UScriptStruct* scriptStruct = FindObject(ANY_PACKAGE, *grpcMessageName, true); 109 | #endif 110 | if (scriptStruct) 111 | { 112 | FName grpcMessageTagName = FName(*(scriptStruct->GetDisplayNameText().ToString())); 113 | GrpcMessageStructMap.Add(grpcMessageTagName, scriptStruct); 114 | } 115 | } 116 | } 117 | #endif 118 | 119 | void FTurboLinkGrpcModule::ShutdownModule() 120 | { 121 | UE_LOG(LogTurboLink, Log, TEXT("Shutdown TurboLinkGrpcModule")); 122 | } 123 | 124 | const UTurboLinkGrpcConfig* FTurboLinkGrpcModule::GetTurboLinkGrpcConfig() const 125 | { 126 | if (!CachedSettings) 127 | { 128 | CachedSettings = GetDefault(); 129 | } 130 | return CachedSettings; 131 | } 132 | 133 | #undef LOCTEXT_NAMESPACE 134 | 135 | IMPLEMENT_MODULE(FTurboLinkGrpcModule, TurboLinkGrpc) 136 | -------------------------------------------------------------------------------- /Source/TurboLinkGrpc/Public/TurboLinkGrpcUtilities.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) Developed by Neo Jin. All Rights Reserved. 2 | #pragma once 3 | 4 | #include "CoreMinimal.h" 5 | #include "Kismet/BlueprintFunctionLibrary.h" 6 | #include "TurboLinkGrpcMessage.h" 7 | #include "TurboLinkGrpcClient.h" 8 | #include "TurboLinkGrpcUtilities.generated.h" 9 | 10 | UCLASS() 11 | class TURBOLINKGRPC_API UTurboLinkGrpcUtilities : public UBlueprintFunctionLibrary 12 | { 13 | GENERATED_BODY() 14 | 15 | public: 16 | UFUNCTION(BlueprintPure, Category = "TurboLink", meta = (WorldContext = "WorldContextObject")) 17 | static class UTurboLinkGrpcManager* GetTurboLinkGrpcManager(UObject* WorldContextObject); 18 | 19 | UFUNCTION(BlueprintPure, Category = "TurboLink") 20 | static const class UTurboLinkGrpcConfig* GetTurboLinkGrpcConfig(); 21 | 22 | //wrap class for uint64 23 | UFUNCTION(BlueprintPure, Category = "TurboLink", DisplayName = "Make UInt64", meta = (AdvancedDisplay = "1")) 24 | static FUInt64 MakeUInt64(FString Value, int Base = 10) 25 | { 26 | return FUInt64{ FCString::Strtoui64(*Value, nullptr, Base) }; 27 | } 28 | 29 | UFUNCTION(BlueprintPure, Category = "TurboLink", DisplayName = "Break UInt64") 30 | static void BreakUInt64(const FUInt64& UInt64, FString& Value) 31 | { 32 | Value = FString::Printf(TEXT("%llu"), UInt64.Value); 33 | } 34 | 35 | //wrap class for uint32 36 | UFUNCTION(BlueprintPure, Category = "TurboLink", DisplayName = "Make UInt32", meta = (AdvancedDisplay = "1")) 37 | static FUInt32 MakeUInt32(FString Value, int Base = 10) 38 | { 39 | return FUInt32{ static_cast((FCString::Strtoui64(*Value, nullptr, Base) & 0xFFFFFFFFull)) }; 40 | } 41 | 42 | UFUNCTION(BlueprintPure, Category = "TurboLink", DisplayName = "Break UInt32") 43 | static void BreakUInt32(const FUInt32& UInt32, FString& Value) 44 | { 45 | Value = FString::Printf(TEXT("%ud"), UInt32.Value); 46 | } 47 | 48 | //wrap class for double 49 | UFUNCTION(BlueprintPure, Category = "TurboLink", DisplayName = "Make Double64") 50 | static FDouble64 MakeDouble64(FString Value) 51 | { 52 | return FDouble64{ FCString::Atod(*Value) }; 53 | } 54 | 55 | UFUNCTION(BlueprintPure, Category = "TurboLink", DisplayName = "Break Double64") 56 | static void BreakDouble64(const FDouble64& Double64, FString& Value) 57 | { 58 | Value = FString::Printf(TEXT("%lf"), Double64.Value); 59 | } 60 | 61 | // Returns true if A is equal to B (A == B) 62 | UFUNCTION(BlueprintPure, meta = (DisplayName = "Equal (GrpcContextHandle)", CompactNodeTitle = "==", Keywords = "== equal", ScriptOperator = "=="), Category = "TurboLink") 63 | static bool EqualEqual_GrpcContextHandle(const FGrpcContextHandle& A, const FGrpcContextHandle& B) 64 | { 65 | return A.Value == B.Value; 66 | } 67 | 68 | UFUNCTION(BlueprintPure, Category = "TurboLink", DisplayName = "Grpc Result To String") 69 | static FString GrpcResultToString(const FGrpcResult& GrpcResult) 70 | { 71 | return GrpcResult.GetMessageString(); 72 | } 73 | 74 | // Generate grpc message from json string 75 | template 76 | static TSharedPtr NewGrpcMessageFromJsonString(const FString& JsonString) 77 | { 78 | UScriptStruct* scriptStruct = T::StaticStruct(); 79 | bool bIsGrpcMessage = scriptStruct->IsChildOf(FGrpcMessage::StaticStruct()); 80 | if (!bIsGrpcMessage) return TSharedPtr(); 81 | 82 | T* newMessage = (T*)FMemory::Malloc(scriptStruct->GetStructureSize()); 83 | scriptStruct->InitializeStruct(newMessage); 84 | bool isSuccess = newMessage->FromJsonString(JsonString); 85 | 86 | //delete memory if failed! 87 | if (!isSuccess) 88 | { 89 | FMemory::Free(newMessage); 90 | return TSharedPtr(); 91 | } 92 | return MakeShareable(newMessage); 93 | } 94 | }; 95 | 96 | UCLASS() 97 | class TURBOLINKGRPC_API UGrpcMessageToJsonFunctionLibrary : public UBlueprintFunctionLibrary 98 | { 99 | GENERATED_BODY() 100 | 101 | public: 102 | UFUNCTION(BlueprintCallable, CustomThunk, meta = (BlueprintInternalUseOnly = "true", CustomStructureParam = "GrpcMessage")) 103 | static FString GrpcMessageToJsonInternal(UStruct* GrpcMessage, bool PrettyMode); 104 | 105 | DECLARE_FUNCTION(execGrpcMessageToJsonInternal); 106 | static FString GrpcMessageToJson_Impl(void* StructPtr, FStructProperty* StructProperty, bool bPrettyMode); 107 | 108 | UFUNCTION(BlueprintCallable, CustomThunk, meta = (BlueprintInternalUseOnly = "true", CustomStructureParam = "ReturnMessage")) 109 | static bool JsonToGrpcMessageInternal(const FString& JsonString, UStruct*& ReturnMessage); 110 | 111 | DECLARE_FUNCTION(execJsonToGrpcMessageInternal); 112 | static bool JsonToGrpcMessage_Impl(const FString& JsonString, void* StructPtr, FStructProperty* StructProperty); 113 | }; 114 | -------------------------------------------------------------------------------- /Source/TurboLinkEditor/Private/SGrpcMessageTagWidget.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) Developed by Neo Jin. All Rights Reserved. 2 | #pragma once 3 | 4 | #include "CoreMinimal.h" 5 | #include "SlateFwd.h" 6 | #include "UObject/Object.h" 7 | #include "Widgets/SWidget.h" 8 | #include "Widgets/SCompoundWidget.h" 9 | #include "Widgets/Views/STreeView.h" 10 | #include "GrpcMessageTagsManager.h" 11 | 12 | // Widget allowing user to tag assets with gameplay tags 13 | class SGrpcMessageTagWidget : public SCompoundWidget 14 | { 15 | public: 16 | 17 | // Called when a tag status is changed 18 | DECLARE_DELEGATE_OneParam(FOnTagChanged, FName) 19 | 20 | SLATE_BEGIN_ARGS(SGrpcMessageTagWidget) 21 | : _SelectedMessageTagName(NAME_None) 22 | , _MaxHeight(260.0f) 23 | {} 24 | SLATE_ARGUMENT(FName, SelectedMessageTagName) // Name of selected message 25 | SLATE_EVENT(FOnTagChanged, OnTagChanged) // Called when a tag status changes 26 | SLATE_ARGUMENT(float, MaxHeight) // caps the height of the gameplay tag tree 27 | SLATE_END_ARGS() 28 | 29 | // Construct the actual widget. 30 | void Construct(const FArguments& InArgs); 31 | 32 | // Ensures that this widget will always account for the MaxHeight if it's specified 33 | virtual FVector2D ComputeDesiredSize(float LayoutScaleMultiplier) const override; 34 | 35 | // Updates the tag list when the filter text changes 36 | void OnFilterTextChanged(const FText& InFilterText); 37 | 38 | // Returns true if this TagNode has any children that match the current filter 39 | bool FilterChildrenCheck(TSharedPtr); 40 | 41 | // Gets the widget to focus once the menu opens. 42 | TSharedPtr GetWidgetToFocusOnOpen(); 43 | 44 | private: 45 | // Verify the selected tags are all valid 46 | void VerifySelectedTagValidity(); 47 | 48 | // Filters the tree view based on the current filter text. 49 | void FilterTagTree(); 50 | 51 | void SelectMessage(const FName& MessageTagName); 52 | 53 | // Holds the full tag name of urrent checked message 54 | FName SelectedMessageTagName; 55 | 56 | // The maximum height of the gameplay tag tree. If 0, the height is unbound. 57 | float MaxHeight; 58 | 59 | // Array of tags to be displayed in the TreeView 60 | TArray< TSharedPtr > TagItems; 61 | 62 | // Container widget holding the tag tree 63 | TSharedPtr TagTreeContainerWidget; 64 | 65 | // Tree widget showing the gameplay tag library 66 | TSharedPtr< STreeView< TSharedPtr > > TagTreeWidget; 67 | 68 | // Allows for the user to find a specific gameplay tag in the tree 69 | TSharedPtr SearchTagBox; 70 | 71 | // Filter string used during search box 72 | FString FilterString; 73 | 74 | // Called when the Tag list changes 75 | FOnTagChanged OnTagChanged; 76 | 77 | /** 78 | * Generate a row widget for the specified item node and table 79 | * 80 | * @param InItem Tag node to generate a row widget for 81 | * @param OwnerTable Table that owns the row 82 | * 83 | * @return Generated row widget for the item node 84 | */ 85 | TSharedRef OnGenerateRow(TSharedPtr InItem, const TSharedRef& OwnerTable); 86 | 87 | /** 88 | * Get children nodes of the specified node 89 | * 90 | * @param InItem Node to get children of 91 | * @param OutChildren [OUT] Array of children nodes, if any 92 | */ 93 | void OnGetChildren(TSharedPtr InItem, TArray< TSharedPtr >& OutChildren); 94 | 95 | /** 96 | * Called via delegate when the status of a check box in a row changes 97 | * 98 | * @param NewCheckState New check box state 99 | * @param NodeChanged Node that was checked/unchecked 100 | */ 101 | void OnTagCheckStatusChanged(ECheckBoxState NewCheckState, TSharedPtr NodeChanged); 102 | 103 | /** 104 | * Called via delegate to determine the checkbox state of the specified node 105 | * 106 | * @param Node Node to find the checkbox state of 107 | * 108 | * @return Checkbox state of the specified node 109 | */ 110 | ECheckBoxState IsTagChecked(TSharedPtr Node) const; 111 | 112 | // is any child node checked 113 | bool IsChildChecked(TSharedPtr Node) const; 114 | 115 | // Called when the user clicks the "Clear Select" button; 116 | FReply OnClearSelectClicked(); 117 | 118 | // Called when the user clicks the "Expand All" button; Expands the entire tag tree 119 | FReply OnExpandAllClicked(); 120 | 121 | // Called when the user clicks the "Collapse All" button; Collapses the entire tag tree 122 | FReply OnCollapseAllClicked(); 123 | 124 | /** 125 | * Helper function to set the expansion state of the tree widget 126 | * 127 | * @param bExpand If true, expand the entire tree; Otherwise, collapse the entire tree 128 | */ 129 | void SetTagTreeItemExpansion(bool bExpand); 130 | 131 | /** 132 | * Helper function to set the expansion state of a specific node 133 | * 134 | * @param Node Node to set the expansion state of 135 | * @param bExapnd If true, expand the node; Otherwise, collapse the node 136 | */ 137 | void SetTagNodeItemExpansion(TSharedPtr Node, bool bExpand); 138 | 139 | // Recursive function to go through all tags in the tree and set the expansion to default 140 | void SetDefaultTagNodeItemExpansion(TSharedPtr Node); 141 | }; 142 | -------------------------------------------------------------------------------- /README_chs.md: -------------------------------------------------------------------------------- 1 | # TurboLink 2 | [English](README.md) | 简体中文 3 | ![logo](https://github.com/thejinchao/turbolink/wiki/image/TurboLink.png) 4 | TurboLink 是一个Unreal Engine插件, 能够在[Unreal Engine](https://www.unrealengine.com/) 中通过C++或者蓝图使用[Google gRPC](https://grpc.io/). 目前可以兼容UE4.27和UE5 5 | 6 | ## 特性 7 | * 跨平台(目前已支持Windows, Linux, Android, iOS, Mac 和 PlayStation5) 8 | * 在C++和蓝图中都可以支持异步调用gRPC函数 9 | * 在C++中支持lambda回调函数 10 | * 在蓝图中支持通过异步节点调用gRPC函数 11 | * 支持流式gRPC函数 12 | * 支持TLS加密链接 13 | * 以protoc插件方式[代码生成工具](https://github.com/thejinchao/protoc-gen-turbolink) ,生成可以在蓝图中直接使用的代码 14 | * 在蓝图中直接构造protobuf消息 15 | * 支持复杂的protobuf消息结构,例如`oneof`字段以及自我引用的消息 16 | * 所有头文件都没有引用gRPC和其他类库头文件,避免工程文件引用过多的头文件 17 | 18 | ## 范例 19 | ![example](https://github.com/thejinchao/turbolink/wiki/image/turbolink_example.png) 20 | 有两个范例工程提供,[simple.demo.56.zip](https://drive.google.com/file/d/11wlpCF6Q4jiyqfaqQTXwwf_bXzA7JyYW/view?usp=sharing) 和 [full.demo.56.zip](https://drive.google.com/file/d/1Nl5h3Nl1zEBoAL1qhpYes-Mf1aXNKRX9/view?usp=sharing) ,建议您首先编译运行范例工程,以了解这个插件是如何运行的。范例工程都包含了可以直接编译运行的UE工程,以及用go语言编写的服务器工程。 21 | ### 运行本地服务器 22 | 1. 安装golang运行环境1.19 23 | 2. 确保当前目录为`TurboLink.example/Server`,然后运行命令`go mod tidy`以更新所有模块 24 | 3. 通过运行命令行`go run main.go`启动服务器 25 | 4. 在UE编辑器中,打开turbolink的设置窗口,然后设置缺省服务器为`localhost:5050`,不能使用`127.0.0.1:5050`,因为范例工程的服务器证书里没有包含这个地址 26 | ### 使用测试服务器 27 | 如果你没有合适的go语言运行环境,可以把客户端链接到我提供的一个互联网上的服务器(grpc.thecodeway.com),我会尽量保证上面的程序一直处于运行状态。 28 | 29 | ## 入门 30 | 31 | ### 1. 安装插件 32 | 1. 从[这里](https://github.com/thejinchao/turbolink/releases) 下载一份插件的release版本 33 | 2. 你也可以用git从这个仓库克隆一份代码到本地,但需要从[这里](https://github.com/thejinchao/turbolink-libraries/releases) 下载一份已经编译好的第三方代码库,然后解压到`Source/ThirdParty`目录里 34 | 3. 在UE工程中创建`Plugin/TurboLink`这个目录,然后把这个插件代码拷贝到里面 35 | 36 | ### 2. 配置服务器地址 37 | 打开编辑器的工程设置界面(TurboLink Grpc/Services Config),在这里可以设置不同的gRPC服务器地址 38 | ![project-setting](https://github.com/thejinchao/turbolink/wiki/image/project-config.png) 39 | 对于没有设置地址的服务,会使用缺省的服务器地址 40 | 41 | ### 3. 配置TLS证书 42 | TurboLink支持服务器端的tls加密链接。 如果你要启用该功能,需要在设置界面(TurboLink Grpc/Services Config)输入服务器的证书文件内容(PEM格式)。由于UE的编辑的文本输入框只支持单行文本,所以需要把证书里的换行符用'\n'代替 43 | ![tls-setting](https://github.com/thejinchao/turbolink/wiki/image/tls-config.png) 44 | 45 | ## 使用方法 46 | 47 | ### 1. 生成协议文件 48 | 加入你的工程里使用的协议文件`hello.proto`内容如下: 49 | ```protobuf 50 | syntax = "proto3"; 51 | 52 | package Greeter; 53 | option go_package = "./Greeter"; 54 | 55 | message HelloRequest { 56 | string name = 1; 57 | } 58 | message HelloResponse { 59 | string reply_message = 1; 60 | } 61 | service GreeterService { 62 | rpc Hello (HelloRequest) returns (HelloResponse); 63 | } 64 | ``` 65 | 如果需要使用该gRPC服务,除了使用`protoc`生成`*.pb.cc` and `*.grpc.pb.cc`外,还需要生成TurboLink插件所需要的一些代码。 66 | 67 | 生成这种文件可以采用两种不同的方法: 68 | ### A. 使用Github actions 69 | 运行 [此Action](https://github.com/thejinchao/turbolink/actions/workflows/compile_proto.yml) - 70 | 点击 `Run Workflow` -> `Run Workflow`. 位于`.github/protos`目录下所有的proto文件就会被自动编译。 71 | 编译结束后,在Summary页面的底部的Artifacts一节中会有生成的C++代码的下载连接。 72 | 如果要编译你自己的proto文件,需要fork本工程,然后把协议文件上传到`.github/protos`目录中 73 | 74 | ### B. 在本地运行编译工具 75 | 在插件的`tools`目录,通过批处理`generate_code.cmd`可以直接生成所有这些文件。在使用这个批处理前,需要确保已经把插件拷贝到工程中,并且已经把编译后的三方库文件拷贝到插件中。运行批处理的命令格式如下: 76 | ``` 77 | generate_code.cmd 78 | ``` 79 | 在上面的例子中,需要以下步骤生成代码: 80 | 1. 运行如下批处理命令:`generate_code.cmd hello.proto .\output_path` 81 | 2. 把生成的代码目录`output_path`中的`Private`和`Public`目录拷贝到插件目录`YourProject/Plugins/TurboLink/Source/TurboLinkGrpc`中 82 | 3. 重新生成工程文件并编译 83 | 84 | 这个批处理使用的是protoc的插件`protoc-gen-turbolink`来生成代码,这个插件的源码在[这里](https://github.com/thejinchao/protoc-gen-turbolink). 不要把工程放在包含有空格的路径之内,以免批处理执行出现错误。 85 | 如果你的工程中包含了多个proto文件,并且文件之间有依赖关系,那么你应该有一个根目录保存这些文件,然后以这个目录作为当前的工作路径来运行`generate_code.cmd` 86 | 87 | ### 2. 链接gRPC服务器 88 | 在C++代码中,使用如下代码连接到gRPC服务器 89 | ```cpp 90 | UTurboLinkGrpcManager* TurboLinkManager = UTurboLinkGrpcUtilities::GetTurboLinkGrpcManager(); 91 | 92 | UGreeterService* GreeterService = Cast(TurboLinkManager->MakeService("GreeterService")); 93 | GreeterService->Connect(); 94 | ``` 95 | 以上代码在蓝图中都有对应的节点可以直接使用 96 | 97 | ### 3. 调用gRPC函数 98 | 有如下几种不同的调用gRPC函数的方法 99 | 100 | #### 3.1 通过Client对象调用 101 | 首先创建服务对应的Client对象,然后通过这个Client对象设置gRPC对应的代理委托函数 102 | ```cpp 103 | GreeterServiceClient = GreeterService->MakeClient(); 104 | GreeterServiceClient->OnHelloResponse.AddUniqueDynamic(this, &UTurboLinkDemoCppTest::OnHelloResponse); 105 | ``` 106 | 然后再调用gRPC函数 107 | ```cpp 108 | FGrpcContextHandle CtxHello = GreeterServiceClient->InitHello(); 109 | 110 | FGrpcGreeterHelloRequest HelloRequest; 111 | HelloRequest.Name = TEXT("Neo"); 112 | 113 | GreeterServiceClient->Hello(CtxHello, HelloRequest); 114 | ``` 115 | 以上函数都有对应的蓝图节点 116 | ![make_client](https://github.com/thejinchao/turbolink/wiki/image/make_client.png) 117 | ![call_grpc](https://github.com/thejinchao/turbolink/wiki/image/call_grpc.png) 118 | 119 | #### 3.2 Lambda回调函数 120 | 如果是一次性调用的gRPC函数,可以再服务创建链接之后,使用使用Lambda回调函数来更快捷使用gRPC接口 121 | ```cpp 122 | FGrpcGreeterHelloRequest HelloRequest; 123 | HelloRequest.Name = TEXT("Neo"); 124 | 125 | GreeterService->CallHello(HelloRequest, 126 | [this](const FGrpcResult& Result, const FGrpcGreeterHelloResponse& Response) 127 | { 128 | if (Result.Code == EGrpcResultCode::Ok) 129 | { 130 | //Do something 131 | } 132 | } 133 | ); 134 | ``` 135 | 需要注意的是,如果是客户端流式类型的函数,是无法使用Lambda回调函数的 136 | 137 | #### 3.3 异步蓝图节点 138 | 在蓝图中,还可以使用异步蓝图节点的方式来更快捷的调用gRPC函数, 使用异步蓝图节点可以一次性完成服务链接,客户端调用回调等过程 139 | ![async-node](https://github.com/thejinchao/turbolink/wiki/image/async-node.png) 140 | 当前异步蓝图节点还无法支持客户端流式以及服务器端流式类型的gRPC函数 141 | 142 | ### 4. 和json互转 143 | 在某些情况下,我们需要将protobuf消息和json字符串互相转换,通过turbolink插件,这种转换也可以在蓝图中操作 144 | #### 4.1 Grpc message to json string 145 | ![message-to-json](https://github.com/thejinchao/turbolink/wiki/image/message_to_json.png) 146 | 结果是`{"name" : "neo"}` 147 | 148 | #### 4.2 Json string to Grpc message 149 | ![json-to-message](https://github.com/thejinchao/turbolink/wiki/image/json_to_message.png) 150 | 151 | 152 | ## 尚不支持的特性 153 | TurboLink的设计目的之一,就是为了能够在蓝图中使用gRPC函数,为了达到这一目的,某些`proto3`中的特性还不被支持。 154 | * 不要使用[`optional`](https://protobuf.dev/programming-guides/proto3/#specifying-field-rules) 类型的字段. 主要是我觉得在蓝图中实现'has_xxx'或者'clean_xxx'这样的特性会导致代码的复杂度大幅度增加,所以暂时没有实现这一特性的计划 155 | * 同样的原因,[`any`](https://protobuf.dev/programming-guides/proto3/#any) 这样的字段目前也没有计划支持 156 | 157 | ## Buy me a coffee 158 | TurboLink是一个完全免费的开源工程,是我在工作之余维护维护的,如果能帮到您,可以考虑点赞支持,多谢! 159 | Buy Me A Coffee 160 | -------------------------------------------------------------------------------- /Source/TurboLinkGrpc/Private/TurboLinkGrpcManager.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) Developed by Neo Jin. All Rights Reserved. 2 | #include "TurboLinkGrpcManager.h" 3 | #include "TurboLinkGrpcManager_Private.h" 4 | #include "TurboLinkGrpcContext.h" 5 | #include "TurboLinkGrpcModule.h" 6 | #include "UObject/UObjectHash.h" 7 | 8 | UTurboLinkGrpcManager::UTurboLinkGrpcManager() 9 | : d (new UTurboLinkGrpcManager::Private()) 10 | , NextTag(0) 11 | { 12 | } 13 | 14 | UTurboLinkGrpcManager::~UTurboLinkGrpcManager() 15 | { 16 | for (auto& CurrentService : WorkingService) 17 | { 18 | CurrentService.Value->Shutdown(); 19 | } 20 | 21 | delete d; 22 | } 23 | 24 | void UTurboLinkGrpcManager::Initialize(FSubsystemCollectionBase& Collection) 25 | { 26 | bIsShutdowning = false; 27 | if (bIsInitialized) return; 28 | UE_LOG(LogTurboLink, Log, TEXT("Initialize TurboLinkGrpcManager")); 29 | 30 | //Registe All Service Classes 31 | TArray seriviceClasses; 32 | GetDerivedClasses(UGrpcService::StaticClass(), seriviceClasses, false); 33 | 34 | for (int32 i = 0; i < seriviceClasses.Num(); i++) 35 | { 36 | UClass* serviceClass = seriviceClasses[i]; 37 | ServiceClassMap.Add(serviceClass->GetName(), serviceClass); 38 | } 39 | //Create global completion queue 40 | d->CompletionQueue = std::make_unique(); 41 | bIsInitialized = true; 42 | } 43 | 44 | void UTurboLinkGrpcManager::Deinitialize() 45 | { 46 | if (bIsShutdowning) return; 47 | bIsShutdowning = true; 48 | UE_LOG(LogTurboLink, Log, TEXT("Deinitialize TurboLinkGrpcManager")); 49 | 50 | //Shutdown all service 51 | for (auto& element : WorkingService) 52 | { 53 | element.Value->Shutdown(); 54 | } 55 | WorkingService.Empty(); 56 | ShutingDownService.Empty(); 57 | 58 | //Shutdown and drain the completion queue 59 | d->ShutdownCompletionQueue(); 60 | d->CompletionQueue = nullptr; 61 | GrpcContextMap.Empty(); 62 | 63 | //clean class map 64 | ServiceClassMap.Empty(); 65 | bIsInitialized = false; 66 | } 67 | 68 | void UTurboLinkGrpcManager::Tick(float DeltaTime) 69 | { 70 | if (bIsShutdowning || !bIsInitialized) return; 71 | 72 | //Check channel state 73 | for (auto& channelElement : d->ChannelMap) 74 | { 75 | std::shared_ptr serviceChannel = channelElement.second; 76 | if (serviceChannel->UpdateState()) 77 | { 78 | EGrpcServiceState newState = Private::GrpcStateToServiceState(serviceChannel->ChannelState); 79 | for (auto& serviceElement : serviceChannel->AttachedServices) 80 | { 81 | serviceElement->OnServiceStateChanged.Broadcast(newState); 82 | } 83 | } 84 | } 85 | 86 | // Loop while listening for completed event. 87 | gpr_timespec deadline; 88 | deadline.clock_type = GPR_CLOCK_MONOTONIC; 89 | deadline.tv_nsec = 1; 90 | deadline.tv_sec = 0; 91 | 92 | while (true) 93 | { 94 | void* event_tag=nullptr; 95 | bool ok; 96 | auto result = d->CompletionQueue->AsyncNext(&event_tag, &ok, deadline); 97 | 98 | if (result == grpc::CompletionQueue::NextStatus::GOT_EVENT) 99 | { 100 | TSharedPtr* grpcContext = GrpcContextMap.Find(event_tag); 101 | if (grpcContext != nullptr) 102 | { 103 | (*grpcContext)->OnRpcEvent(ok, event_tag); 104 | } 105 | } 106 | else 107 | { 108 | //time out 109 | break; 110 | } 111 | } 112 | 113 | //get config instance 114 | FTurboLinkGrpcModule* turboLinkModule = FModuleManager::GetModulePtr("TurboLinkGrpc"); 115 | const UTurboLinkGrpcConfig* config = turboLinkModule->GetTurboLinkGrpcConfig(); 116 | int keepServiceAliveWithoutRefrenceSeconds = config->KeepServiceAliveWithoutRefrenceSeconds; 117 | 118 | //Tick working service 119 | for (auto it = WorkingService.CreateIterator(); it; ++it) 120 | { 121 | UGrpcService* service = it.Value(); 122 | service->Tick(DeltaTime); 123 | 124 | //pending shutdown? 125 | if (service->RefrenceCounts <= 0 && service->StartPendingShutdown > 0.0 && keepServiceAliveWithoutRefrenceSeconds > 0) 126 | { 127 | double secondsNow = FPlatformTime::Seconds(); 128 | if (secondsNow - service->StartPendingShutdown > keepServiceAliveWithoutRefrenceSeconds) 129 | { 130 | //remove from working map and add to shutingdown set 131 | it.RemoveCurrent(); 132 | ShutingDownService.Add(service); 133 | 134 | //call real shutdown function 135 | service->StartPendingShutdown = 0.0; 136 | service->Shutdown(); 137 | 138 | UE_LOG(LogTurboLink, Log, TEXT("Shutdown service[%s]"), *(service->GetName())); 139 | } 140 | } 141 | } 142 | WorkingService.Compact(); 143 | 144 | //Tick shuting down service 145 | for (auto it = ShutingDownService.CreateIterator(); it; ++it) 146 | { 147 | UGrpcService* service = *it; 148 | service->Tick(DeltaTime); 149 | 150 | if (service->bIsShutingDown && service->ClientSet.Num() == 0) 151 | { 152 | //Remove shutdowned service 153 | it.RemoveCurrent(); 154 | } 155 | } 156 | ShutingDownService.Compact(); 157 | } 158 | 159 | UGrpcService* UTurboLinkGrpcManager::MakeService(const FString& ServiceName) 160 | { 161 | UGrpcService** workingService = WorkingService.Find(ServiceName); 162 | 163 | //find existent working service 164 | if (workingService != nullptr) 165 | { 166 | //add refrence 167 | (*workingService)->RefrenceCounts += 1; 168 | (*workingService)->StartPendingShutdown = 0.0; 169 | 170 | UE_LOG(LogTurboLink, Log, TEXT("MakeService service[%s], RefrenceCounts=[%d]"), *ServiceName, (*workingService)->RefrenceCounts); 171 | return *workingService; 172 | } 173 | 174 | //create new service object 175 | UClass** serviceClass = ServiceClassMap.Find(ServiceName); 176 | if (serviceClass == nullptr) 177 | { 178 | UE_LOG(LogTurboLink, Error, TEXT("Can't find service class[%s]"), *ServiceName); 179 | return nullptr; 180 | } 181 | 182 | UGrpcService* service = NewObject(this, *serviceClass); 183 | service->TurboLinkManager = this; 184 | service->RefrenceCounts = 1; 185 | WorkingService.Add(ServiceName, service); 186 | return service; 187 | } 188 | 189 | void UTurboLinkGrpcManager::ReleaseService(UGrpcService* Service) 190 | { 191 | check(Service); 192 | 193 | //release refrence 194 | if (Service->RefrenceCounts > 1) 195 | { 196 | Service->RefrenceCounts -= 1; 197 | UE_LOG(LogTurboLink, Log, TEXT("ReleaseService service[%s], RefrenceCounts=[%d]"), *(Service->GetName()), Service->RefrenceCounts); 198 | return; 199 | } 200 | 201 | //pending to shutdown 202 | Service->RefrenceCounts = 0; 203 | Service->StartPendingShutdown = FPlatformTime::Seconds(); 204 | 205 | UE_LOG(LogTurboLink, Log, TEXT("ReleaseService service[%s], StartPendingShutdown..."), *(Service->GetName())); 206 | } 207 | 208 | EGrpcServiceState UTurboLinkGrpcManager::GetServiceState(UGrpcService* Service) 209 | { 210 | check(Service != nullptr); 211 | 212 | return Service->GetServiceState(); 213 | } 214 | 215 | void* UTurboLinkGrpcManager::GetNextTag(TSharedPtr Context) 216 | { 217 | void* nextTag = (void*)(++NextTag); 218 | GrpcContextMap.Add(nextTag, Context); 219 | return nextTag; 220 | } 221 | 222 | void UTurboLinkGrpcManager::RemoveTag(void* Tag) 223 | { 224 | GrpcContextMap.Remove(Tag); 225 | } 226 | 227 | FGrpcContextHandle UTurboLinkGrpcManager::GetNextContextHandle() 228 | { 229 | FGrpcContextHandle nextHandle(++NextContextHandle); 230 | return nextHandle; 231 | } 232 | -------------------------------------------------------------------------------- /Source/TurboLinkEditor/Private/GrpcMessageTagsManager.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) Developed by Neo Jin. All Rights Reserved. 2 | #include "GrpcMessageTagsManager.h" 3 | #include "Misc/OutputDeviceNull.h" 4 | #include "TurboLinkGrpcModule.h" 5 | 6 | UGrpcMessageTagsManager* UGrpcMessageTagsManager::Singleton = nullptr; 7 | 8 | FGrpcMessageTag::FGrpcMessageTag(FName _SimpleTagName, FName _TagName, TSharedPtr _ParentNode, UScriptStruct* _MessageScriptStruct) 9 | : SimpleTagName(_SimpleTagName) 10 | , TagName(_TagName) 11 | , ParentNode(_ParentNode) 12 | , MessageScriptStruct(_MessageScriptStruct) 13 | { 14 | } 15 | 16 | void FGrpcMessageTag::ResetNode() 17 | { 18 | SimpleTagName = NAME_None; 19 | TagName = NAME_None; 20 | for (int32 ChildIdx = 0; ChildIdx < ChildTags.Num(); ++ChildIdx) 21 | { 22 | ChildTags[ChildIdx]->ResetNode(); 23 | } 24 | 25 | ChildTags.Empty(); 26 | ParentNode.Reset(); 27 | } 28 | 29 | void FGrpcMessageTag::FromExportString(const FString& ExportString, int32 PortFlags) 30 | { 31 | ResetNode(); 32 | 33 | FOutputDeviceNull NullOut; 34 | FGrpcMessageTag::StaticStruct()->ImportText(*ExportString, this, nullptr, PortFlags, &NullOut, TEXT("FGrpcMessageTag"), true); 35 | 36 | //Split tagname to get simple name 37 | TArray SubTags; 38 | TagName.ToString().ParseIntoArray(SubTags, TEXT("."), true); 39 | if (SubTags.Num() > 0) 40 | { 41 | SimpleTagName = FName(*SubTags.Last()); 42 | } 43 | } 44 | 45 | UGrpcMessageTagsManager::UGrpcMessageTagsManager(const FObjectInitializer& ObjectInitializer) 46 | : Super(ObjectInitializer) 47 | { 48 | } 49 | 50 | UGrpcMessageTagsManager::~UGrpcMessageTagsManager() 51 | { 52 | DestroyTagTree(); 53 | Singleton = nullptr; 54 | } 55 | 56 | void UGrpcMessageTagsManager::InitializeManager() 57 | { 58 | check(!Singleton); 59 | 60 | Singleton = NewObject(GetTransientPackage(), NAME_None); 61 | Singleton->AddToRoot(); 62 | 63 | //construct tag tree 64 | Singleton->ConstructTagTree(); 65 | } 66 | 67 | void UGrpcMessageTagsManager::ConstructTagTree() 68 | { 69 | TGuardValue GuardRebuilding(bIsConstructingTagTree, true); 70 | if (GrpcMessageRootTag.IsValid()) return; 71 | 72 | GrpcMessageRootTag = MakeShareable(new FGrpcMessageTag()); 73 | 74 | //Scan all grpc messages... 75 | FTurboLinkGrpcModule* turboLinkModule = FModuleManager::GetModulePtr("TurboLinkGrpc"); 76 | if (turboLinkModule == nullptr) 77 | { 78 | turboLinkModule = &(FModuleManager::LoadModuleChecked("TurboLinkGrpc")); 79 | } 80 | if (turboLinkModule) 81 | { 82 | const TMap& messageStructMap = turboLinkModule->GetMessageStructMap(); 83 | for (const auto& elem : messageStructMap) 84 | { 85 | AddGrpcMessageTag(elem.Key, elem.Value); 86 | } 87 | } 88 | } 89 | 90 | void UGrpcMessageTagsManager::DestroyTagTree() 91 | { 92 | if (GrpcMessageRootTag.IsValid()) 93 | { 94 | GrpcMessageRootTag->ResetNode(); 95 | GrpcMessageRootTag.Reset(); 96 | } 97 | } 98 | 99 | void UGrpcMessageTagsManager::AddGrpcMessageTag(const FName& OriginalTagName, UScriptStruct* MessageScriptStruct) 100 | { 101 | TSharedPtr CurNode = GrpcMessageRootTag; 102 | 103 | // Split the tag text on the "." delimiter to establish tag depth and then insert each tag into the tag tree 104 | TArray SubTags; 105 | OriginalTagName.ToString().ParseIntoArray(SubTags, TEXT("."), true); 106 | 107 | int32 NumSubTags = SubTags.Num(); 108 | bool bHasSeenConflict = false; 109 | FString FullTagString; 110 | 111 | for (int32 SubTagIdx = 0; SubTagIdx < NumSubTags; ++SubTagIdx) 112 | { 113 | bool bIsExplicitTag = (SubTagIdx == (NumSubTags - 1)); 114 | FName ShortTagName = *SubTags[SubTagIdx]; 115 | FName FullTagName; 116 | 117 | if (bIsExplicitTag) 118 | { 119 | // We already know the final name 120 | FullTagName = OriginalTagName; 121 | } 122 | else if (SubTagIdx == 0) 123 | { 124 | // Full tag is the same as short tag, and start building full tag string 125 | FullTagName = ShortTagName; 126 | FullTagString = SubTags[SubTagIdx]; 127 | } 128 | else 129 | { 130 | // Add .Tag and use that as full tag 131 | FullTagString += TEXT("."); 132 | FullTagString += SubTags[SubTagIdx]; 133 | 134 | FullTagName = FName(*FullTagString); 135 | } 136 | 137 | TArray< TSharedPtr >& ChildTags = CurNode.Get()->GetChildTagNodes(); 138 | int32 InsertionIdx = InsertTagIntoNodeArray(ShortTagName, FullTagName, CurNode, ChildTags, bIsExplicitTag? MessageScriptStruct: nullptr); 139 | 140 | CurNode = ChildTags[InsertionIdx]; 141 | } 142 | } 143 | 144 | void UGrpcMessageTagsManager::GetGrpcMessageRootTags(TArray< TSharedPtr >& OutTagArray) const 145 | { 146 | TArray>& grpcMessageRootTags = GrpcMessageRootTag->GetChildTagNodes(); 147 | OutTagArray = grpcMessageRootTags; 148 | } 149 | 150 | TSharedPtr UGrpcMessageTagsManager::FindGrpcMessageTag(const FName& TagName, bool bWithScriptStructOnly) 151 | { 152 | const TArray>& tagsArray = GrpcMessageRootTag->GetChildTagNodes(); 153 | 154 | return FindGrpcMessageTagInArray(tagsArray, TagName, bWithScriptStructOnly); 155 | } 156 | 157 | TSharedPtr UGrpcMessageTagsManager::FindGrpcMessageTagInArray(const TArray>& grpcMessageTags, 158 | const FName& TagName, bool bWithScriptStructOnly) 159 | { 160 | for (int32 iItem = 0; iItem < grpcMessageTags.Num(); ++iItem) 161 | { 162 | if (grpcMessageTags[iItem]->TagName == TagName && 163 | (!bWithScriptStructOnly || (bWithScriptStructOnly && grpcMessageTags[iItem]->MessageScriptStruct != nullptr))) 164 | { 165 | return grpcMessageTags[iItem]; 166 | } 167 | 168 | if (grpcMessageTags[iItem]->TagName.GetStringLength() < TagName.GetStringLength()) 169 | { 170 | TSharedPtr messageTag = FindGrpcMessageTagInArray(grpcMessageTags[iItem]->GetChildTagNodes(), 171 | TagName, bWithScriptStructOnly); 172 | if (messageTag.IsValid()) return messageTag; 173 | } 174 | } 175 | return TSharedPtr(); 176 | } 177 | 178 | int32 UGrpcMessageTagsManager::InsertTagIntoNodeArray(FName Tag, FName FullTag, 179 | TSharedPtr ParentNode, TArray< TSharedPtr >& NodeArray, UScriptStruct* MessageScriptStruct) 180 | { 181 | int32 FoundNodeIdx = INDEX_NONE; 182 | int32 WhereToInsert = INDEX_NONE; 183 | 184 | // See if the tag is already in the array 185 | for (int32 CurIdx = 0; CurIdx < NodeArray.Num(); ++CurIdx) 186 | { 187 | FGrpcMessageTag* CurrNode = NodeArray[CurIdx].Get(); 188 | if (CurrNode == nullptr) continue; 189 | 190 | FName SimpleTagName = CurrNode->SimpleTagName; 191 | if (SimpleTagName == Tag) 192 | { 193 | FoundNodeIdx = CurIdx; 194 | break; 195 | } 196 | else if (Tag.LexicalLess(SimpleTagName) && WhereToInsert == INDEX_NONE) 197 | { 198 | // Insert new node before this 199 | WhereToInsert = CurIdx; 200 | } 201 | } 202 | 203 | if (MessageScriptStruct != nullptr && FoundNodeIdx != INDEX_NONE) 204 | { 205 | FGrpcMessageTag* CurrNode = NodeArray[FoundNodeIdx].Get(); 206 | CurrNode->MessageScriptStruct = MessageScriptStruct; 207 | } 208 | 209 | if (FoundNodeIdx == INDEX_NONE) 210 | { 211 | if (WhereToInsert == INDEX_NONE) 212 | { 213 | // Insert at end 214 | WhereToInsert = NodeArray.Num(); 215 | } 216 | 217 | // Don't add the root node as parent 218 | TSharedPtr TagNode = 219 | MakeShareable(new FGrpcMessageTag(Tag, FullTag, ParentNode == GrpcMessageRootTag ? nullptr : ParentNode, MessageScriptStruct)); 220 | // Add at the sorted location 221 | FoundNodeIdx = NodeArray.Insert(TagNode, WhereToInsert); 222 | } 223 | return FoundNodeIdx; 224 | } -------------------------------------------------------------------------------- /Source/TurboLinkEditor/Private/GrpcMessageK2Node_MessageToJson.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) Developed by Neo Jin. All Rights Reserved. 2 | #include "GrpcMessageK2Node_MessageToJson.h" 3 | #include "EdGraphSchema_K2.h" 4 | #include "BlueprintNodeSpawner.h" 5 | #include "BlueprintActionDatabaseRegistrar.h" 6 | #include "K2Node_CallFunction.h" 7 | #include "KismetCompiler.h" 8 | #include "Kismet2/BlueprintEditorUtils.h" 9 | #include "K2Node_MakeStruct.h" 10 | #include "TurboLinkGrpcMessage.h" 11 | #include "TurboLinkGrpcUtilities.h" 12 | 13 | #define LOCTEXT_NAMESPACE "K2Node_TurboLinkGrpcMessageToJsonNode" 14 | 15 | struct FK2Node_GrpcMessageToJsonHelper 16 | { 17 | static FName MessagePinName; 18 | static FName PrettyModeName; 19 | }; 20 | 21 | FName FK2Node_GrpcMessageToJsonHelper::MessagePinName(TEXT("GrpcMessage")); 22 | FName FK2Node_GrpcMessageToJsonHelper::PrettyModeName(TEXT("PrettyMode")); 23 | 24 | void UGrpcMessageToJsonNode::GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const 25 | { 26 | UClass* ActionKey = GetClass(); 27 | if (ActionRegistrar.IsOpenForRegistration(ActionKey)) 28 | { 29 | UBlueprintNodeSpawner* NodeSpawner = UBlueprintNodeSpawner::Create(GetClass()); 30 | check(NodeSpawner != nullptr); 31 | 32 | ActionRegistrar.AddBlueprintAction(ActionKey, NodeSpawner); 33 | } 34 | } 35 | 36 | FText UGrpcMessageToJsonNode::GetMenuCategory() const 37 | { 38 | return LOCTEXT("MenuCategory", "TurboLink"); 39 | } 40 | 41 | FText UGrpcMessageToJsonNode::GetNodeTitle(ENodeTitleType::Type Title) const 42 | { 43 | bool bLineked; 44 | UScriptStruct* structType = GetLinkedStruct(bLineked); 45 | if (structType == nullptr) 46 | { 47 | return LOCTEXT("DefaultTitle", "GrpcMessage To Json String"); 48 | } 49 | 50 | if (CachedNodeTitle.IsOutOfDate(this)) 51 | { 52 | FFormatNamedArguments Args; 53 | Args.Add(TEXT("Message"), FText::FromName(structType->GetFName())); 54 | CachedNodeTitle.SetCachedText(FText::Format(LOCTEXT("NodeTitle", "{Message} To Json String"), Args), this); 55 | } 56 | 57 | return CachedNodeTitle; 58 | } 59 | 60 | FText UGrpcMessageToJsonNode::GetTooltipText() const 61 | { 62 | return FText::FromString(TEXT("Convert GrpcMessage to Json String")); 63 | } 64 | 65 | void UGrpcMessageToJsonNode::GetPinHoverText(const UEdGraphPin& Pin, FString& HoverTextOut) const 66 | { 67 | if (Pin.PinName == FK2Node_GrpcMessageToJsonHelper::MessagePinName) 68 | { 69 | //set friend message name 70 | bool bConnected = false; 71 | UScriptStruct* scriptStruct = GetLinkedStruct(bConnected); 72 | if (scriptStruct != nullptr && bConnected) 73 | { 74 | HoverTextOut = FString::Printf(TEXT("Grpc Message\n%s"), *(scriptStruct->GetFName().ToString())); 75 | return; 76 | } 77 | } 78 | Super::GetPinHoverText(Pin, HoverTextOut); 79 | } 80 | 81 | void UGrpcMessageToJsonNode::AllocateDefaultPins() 82 | { 83 | // Add execution pins 84 | UEdGraphPin* execPin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Execute); 85 | UEdGraphPin* thenPin = CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Then); 86 | 87 | // Add Message pin 88 | UEdGraphPin* messagePin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Wildcard, FK2Node_GrpcMessageToJsonHelper::MessagePinName); 89 | // Add Pretty Mode pin 90 | UEdGraphPin* prettyModePin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Boolean, FK2Node_GrpcMessageToJsonHelper::PrettyModeName); 91 | 92 | // return value pin 93 | UEdGraphPin* returnValuePin = CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_String, UEdGraphSchema_K2::PN_ReturnValue); 94 | 95 | Super::AllocateDefaultPins(); 96 | } 97 | 98 | void UGrpcMessageToJsonNode::ExpandNode(FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph) 99 | { 100 | Super::ExpandNode(CompilerContext, SourceGraph); 101 | 102 | UEdGraphPin* execPin = GetExecPin(); 103 | UEdGraphPin* messagePin = GetInputPin(FK2Node_GrpcMessageToJsonHelper::MessagePinName); 104 | UEdGraphPin* prettyModePin = GetInputPin(FK2Node_GrpcMessageToJsonHelper::PrettyModeName); 105 | UEdGraphPin* returnValuePin = GetResultPin(); 106 | UEdGraphPin* thenPin = GetThenPin(); 107 | 108 | //is linked to a valid make grpc message struct node? 109 | bool bLineked; 110 | UScriptStruct* structType = GetLinkedStruct(bLineked); 111 | if (bLineked && structType == nullptr) 112 | { 113 | //compile error 114 | CompilerContext.MessageLog.Error(TEXT("Only accept struct inherited from the FGrpcMessage. @@"), this); 115 | BreakAllNodeLinks(); 116 | return; 117 | } 118 | 119 | // not working yet! 120 | if (execPin == nullptr || thenPin == nullptr) 121 | { 122 | BreakAllNodeLinks(); 123 | return; 124 | } 125 | 126 | // here we adapt/bind our pins to the static function pins that we are calling. 127 | UK2Node_CallFunction* callFunctionNode = CompilerContext.SpawnIntermediateNode(this, SourceGraph); 128 | static FName InternalFunctionName = GET_FUNCTION_NAME_CHECKED(UGrpcMessageToJsonFunctionLibrary, GrpcMessageToJsonInternal); 129 | callFunctionNode->FunctionReference.SetExternalMember(InternalFunctionName, UGrpcMessageToJsonFunctionLibrary::StaticClass()); 130 | callFunctionNode->AllocateDefaultPins(); 131 | 132 | // get pins from call function node 133 | UEdGraphPin* callFunction_ExecPin = callFunctionNode->GetExecPin(); 134 | UEdGraphPin* callFunction_MessagePin = callFunctionNode->FindPinChecked(FK2Node_GrpcMessageToJsonHelper::MessagePinName); 135 | UEdGraphPin* callFunction_PrettyModePin = callFunctionNode->FindPinChecked(FK2Node_GrpcMessageToJsonHelper::PrettyModeName); 136 | UEdGraphPin* callFunction_ReturnValuePin = callFunctionNode->GetReturnValuePin(); 137 | UEdGraphPin* callFunction_ThenPin = callFunctionNode->GetThenPin(); 138 | 139 | //grpc message in 140 | if (messagePin && messagePin->LinkedTo.Num()>0) 141 | { 142 | //assign input struct type to the wildcard input node type of call function node 143 | UEdGraphPin* linkedMessageStruct = messagePin->LinkedTo[0]; 144 | CompilerContext.MovePinLinksToIntermediate(*messagePin, *callFunction_MessagePin); 145 | callFunction_MessagePin->PinType = linkedMessageStruct->PinType; 146 | } 147 | // pretty mode pin 148 | CompilerContext.MovePinLinksToIntermediate(*prettyModePin, *callFunction_PrettyModePin); 149 | // return value pin 150 | CompilerContext.MovePinLinksToIntermediate(*returnValuePin, *callFunction_ReturnValuePin); 151 | //exec pin 152 | CompilerContext.MovePinLinksToIntermediate(*execPin, *callFunction_ExecPin); 153 | //then pin 154 | CompilerContext.MovePinLinksToIntermediate(*thenPin, *callFunction_ThenPin); 155 | // break any links to the expanded node 156 | BreakAllNodeLinks(); 157 | } 158 | 159 | UEdGraphPin* UGrpcMessageToJsonNode::GetThenPin() const 160 | { 161 | UEdGraphPin* Pin = FindPinChecked(UEdGraphSchema_K2::PN_Then); 162 | check(Pin->Direction == EGPD_Output); 163 | return Pin; 164 | } 165 | 166 | UEdGraphPin* UGrpcMessageToJsonNode::GetInputPin(const FName& PinName) const 167 | { 168 | UEdGraphPin* Pin = nullptr; 169 | for (UEdGraphPin* TestPin : Pins) 170 | { 171 | if (TestPin && TestPin->PinName == PinName) 172 | { 173 | Pin = TestPin; 174 | break; 175 | } 176 | } 177 | check(Pin == nullptr || Pin->Direction == EGPD_Input); 178 | return Pin; 179 | } 180 | 181 | UEdGraphPin* UGrpcMessageToJsonNode::GetResultPin() const 182 | { 183 | UEdGraphPin* Pin = FindPinChecked(UEdGraphSchema_K2::PN_ReturnValue); 184 | check(Pin->Direction == EGPD_Output); 185 | return Pin; 186 | } 187 | 188 | UScriptStruct* UGrpcMessageToJsonNode::GetLinkedStruct(bool& bLinked) const 189 | { 190 | UEdGraphPin* messagePin = GetInputPin(FK2Node_GrpcMessageToJsonHelper::MessagePinName); 191 | 192 | //is linked to a valid make grpc message struct node? 193 | if (messagePin==nullptr || messagePin->LinkedTo.Num() == 0) 194 | { 195 | bLinked = false; 196 | return nullptr; 197 | } 198 | 199 | bLinked = true; 200 | UEdGraphPin* messagePinLink = messagePin->LinkedTo[0]; 201 | if (messagePinLink && messagePinLink->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct) 202 | { 203 | UScriptStruct* structType = Cast(messagePinLink->PinType.PinSubCategoryObject.Get()); 204 | if (structType && structType->IsChildOf(FGrpcMessage::StaticStruct())) 205 | { 206 | return structType; 207 | } 208 | } 209 | 210 | return nullptr; 211 | } 212 | 213 | void UGrpcMessageToJsonNode::PinConnectionListChanged(UEdGraphPin* Pin) 214 | { 215 | Super::PinConnectionListChanged(Pin); 216 | 217 | if (Pin && (Pin->PinName == FK2Node_GrpcMessageToJsonHelper::MessagePinName)) 218 | { 219 | OnMessagePinChanged(); 220 | } 221 | } 222 | 223 | void UGrpcMessageToJsonNode::PinDefaultValueChanged(UEdGraphPin* ChangedPin) 224 | { 225 | if (ChangedPin && (ChangedPin->PinName == FK2Node_GrpcMessageToJsonHelper::MessagePinName)) 226 | { 227 | OnMessagePinChanged(); 228 | } 229 | } 230 | 231 | void UGrpcMessageToJsonNode::OnMessagePinChanged() 232 | { 233 | UEdGraphPin* MessagePin = GetInputPin(FK2Node_GrpcMessageToJsonHelper::MessagePinName); 234 | 235 | // Mark dirty 236 | CachedNodeTitle.MarkDirty(); 237 | FBlueprintEditorUtils::MarkBlueprintAsModified(GetBlueprint()); 238 | } 239 | 240 | #undef LOCTEXT_NAMESPACE 241 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TurboLink 2 | English | [简体中文](README_chs.md) 3 | ![logo](https://github.com/thejinchao/turbolink/wiki/image/TurboLink.png) 4 | TurboLink is an unreal engine plugin that enables [Google gRPC](https://grpc.io/) to work with [Unreal Engine](https://www.unrealengine.com/) using C++ or Blueprint. It is compatible with UE 4.27 and 5. 5 | 6 | ## Features 7 | * Cross-platform ready. (Windows, Linux, Android, iOS, Mac, and PlayStation5) 8 | * Call gRPC functions asynchronously in C++ and blueprint. 9 | * Support lambda callback and delegate function in C++. 10 | * Support async blueprint node to quickly call gRPC functions in a blueprint. 11 | * Support streaming gRPC methods. 12 | * Support TLS connection. 13 | * A [protoc-plugin code generation tool](https://github.com/thejinchao/protoc-gen-turbolink) for generating protobuf code wrappers that can be used directly in blueprint. 14 | * Construct protobuf message through native make nodes in blueprints. 15 | * Support complex protobuf structures such as `oneof` field and self-nesting struct. 16 | * All public header files in the plugin do not include gRPC and protobuf library header files so that your project avoids including too many header files. 17 | 18 | ## Example 19 | ![example](https://github.com/thejinchao/turbolink/wiki/image/turbolink_example.png) 20 | Two example projects can be downloaded, [simple.demo.56.zip](https://drive.google.com/file/d/11wlpCF6Q4jiyqfaqQTXwwf_bXzA7JyYW/view?usp=sharing) and [full.demo.56.zip](https://drive.google.com/file/d/1Nl5h3Nl1zEBoAL1qhpYes-Mf1aXNKRX9/view?usp=sharing) 21 | It is recommended that first download example projects and run them to understand how the plugin works. All demo projects include UE projects and server projects that can be run directly. 22 | 23 | ### Run local server 24 | 1. Installl golang enviroment 1.19 25 | 2. Make sure currennt directory is `TurboLink.example/Server`, and run `go mod tidy` to update all module needed. 26 | 3. Run grpc service with command `go run main.go` 27 | 4. Open turbolink setting windows in UE editor, set default Endpoint as `localhost:5050`. Do not use `127.0.0.1:5050` because the certificate file in the sample project does not include this domain 28 | ### Public test server 29 | If you do not have a golang runtime environment, you can connect the client to the server I provided (grpc.thecodeway.com). I will try to keep this server running. 30 | 31 | ## Geting started 32 | 33 | ### 1. Installing the plugin 34 | 1. Download a release version from [here](https://github.com/thejinchao/turbolink/releases). 35 | 2. You can also clone this repo locally through git, but you also need to download pre-built third party binaries libraries from [here](https://github.com/thejinchao/turbolink-libraries/releases), and extract it to `Source/ThirdParty`. 36 | 3. Create a `Plugins/TurboLink` folder under your project folder, then copy this repo into it. 37 | 38 | ### 2. Config service endpoint 39 | Open the project setting window (TurboLink Grpc/Services Config) to set the server endpoint to different gRPC services. 40 | ![project-setting](https://github.com/thejinchao/turbolink/wiki/image/project-config.png) 41 | For services that do not have an endpoint set, turbolink will use the default endpoint to connect. 42 | 43 | ### 3. Config TLS certificate 44 | Turbolink support server-side tls connection type. If you want to enable this function, you need to set the server certificate file(PEM format) in the settings windows (TurboLink Grpc/Services Config). Because UE's setting window only supports single-line text, you need to replace the newline character in the certificate file with `\n`. 45 | ![tls-setting](https://github.com/thejinchao/turbolink/wiki/image/tls-config.png) 46 | 47 | ## Usage 48 | 49 | ### 1. Generate code from gRPC file 50 | For example, a simple gRPC service `hello.proto` is as follows: 51 | ```protobuf 52 | syntax = "proto3"; 53 | 54 | package Greeter; 55 | option go_package = "./Greeter"; 56 | 57 | message HelloRequest { 58 | string name = 1; 59 | } 60 | message HelloResponse { 61 | string reply_message = 1; 62 | } 63 | service GreeterService { 64 | rpc Hello (HelloRequest) returns (HelloResponse); 65 | } 66 | ``` 67 | To use this service, in addition to using `protoc` to generate `*.pb.cc` and `*.grpc.pb.cc` files, you also need to generate the code files required by turbolink. 68 | 69 | There are 2 options to do so: 70 | ### A. Using Github actions 71 | Follow [this](https://github.com/thejinchao/turbolink/actions/workflows/compile_proto.yml) - 72 | Click on `Run Workflow` -> `Run Workflow`. This will build all proto files in the `.github/protos` directory. 73 | Then at the end of the build - at the bottom of the summary page you will see the "Generated C++ Code" download link under Artifacts section. 74 | To use your own .proto - you can clone/fork this repo and add your proto files in the `.github/protos` directory. 75 | 76 | ### B. Running the tool locally 77 | In the `tools` directory of the plugin, there is a batch file called `generate_code.cmd` that is used to generate all the gRPC code files. Before using it, make sure you have installed the plugin into your project and all third-party library files are installed. The command line is: 78 | ``` 79 | generate_code.cmd 80 | ``` 81 | In the proto file above, Use the following steps to generate code files: 82 | 1. Generate code file with command line: `generate_code.cmd hello.proto .\output_path` 83 | 2. Copy generated directories `Private` and `Public` from `output_path` to `YourProject/Plugins/TurboLink/Source/TurboLinkGrpc` 84 | 3. Re-generate your project solution and build it. 85 | 86 | This batch file generates code through a protoc plugin named `protoc-gen-turbolink`, the code of this plugin can be found [here](https://github.com/thejinchao/protoc-gen-turbolink). Do not put the project in the path containing spaces to avoid errors in execution. 87 | If your project contains multiple proto files, and there are dependencies between files, then you should have a root directory to save these files, and then use this directory as the current working path to run `generate_code.cmd` 88 | 89 | ### 2. Connect to gRPC service 90 | Use the following c++ code to link to the gRPC services. 91 | ```cpp 92 | UTurboLinkGrpcManager* TurboLinkManager = UTurboLinkGrpcUtilities::GetTurboLinkGrpcManager(); 93 | 94 | UGreeterService* GreeterService = Cast(TurboLinkManager->MakeService("GreeterService")); 95 | GreeterService->Connect(); 96 | ``` 97 | The above functions can be called directly in the blueprint. 98 | 99 | ### 3. Call gRPC methods 100 | There are several different ways of calling gRPC methods. 101 | 102 | #### 3.1 Client object 103 | First, create the client object, and set the delegate function. 104 | ```cpp 105 | GreeterServiceClient = GreeterService->MakeClient(); 106 | GreeterServiceClient->OnHelloResponse.AddUniqueDynamic(this, &UTurboLinkDemoCppTest::OnHelloResponse); 107 | ``` 108 | Then create a context object and call the gRPC method. 109 | ```cpp 110 | FGrpcContextHandle CtxHello = GreeterServiceClient->InitHello(); 111 | 112 | FGrpcGreeterHelloRequest HelloRequest; 113 | HelloRequest.Name = TEXT("Neo"); 114 | 115 | GreeterServiceClient->Hello(CtxHello, HelloRequest); 116 | ``` 117 | The above functions can be called directly in the blueprint. 118 | ![make_client](https://github.com/thejinchao/turbolink/wiki/image/make_client.png) 119 | ![call_grpc](https://github.com/thejinchao/turbolink/wiki/image/call_grpc.png) 120 | 121 | #### 3.2 Lambda callback 122 | If the gRPC call is a one-off, you can use a lambda function as a callback after the service is connected. 123 | ```cpp 124 | FGrpcGreeterHelloRequest HelloRequest; 125 | HelloRequest.Name = TEXT("Neo"); 126 | 127 | GreeterService->CallHello(HelloRequest, 128 | [this](const FGrpcResult& Result, const FGrpcGreeterHelloResponse& Response) 129 | { 130 | if (Result.Code == EGrpcResultCode::Ok) 131 | { 132 | //Do something 133 | } 134 | } 135 | ); 136 | ``` 137 | It should be noted that if it is a function of client stream type, lambda callback cannot be used. 138 | 139 | #### 3.3 Async blueprint node 140 | In the blueprint, if you need to quickly test some gRPC functions, or use some one-off functions, you can use an asynchronous blueprint node, which can automatically complete the service link and callback processing. 141 | ![async-node](https://github.com/thejinchao/turbolink/wiki/image/async-node.png) 142 | Currently, the async node cannot support gRPC functions of client stream and server stream types. 143 | 144 | ### 4. Conver with json string 145 | In some cases, we need to convert protobuf messages and json strings to each other. Through the turbolink, this conversion can also be operated in the blueprint 146 | #### 4.1 Grpc message to json string 147 | ![message-to-json](https://github.com/thejinchao/turbolink/wiki/image/message_to_json.png) 148 | The result is `{"name" : "neo"}` 149 | 150 | #### 4.2 Json string to Grpc message 151 | ![json-to-message](https://github.com/thejinchao/turbolink/wiki/image/json_to_message.png) 152 | 153 | 154 | ## Feature not yet implemented 155 | One of the design purposes of TurboLink is to be able to use the gRPC directly in the blueprint, so some `proto3` features cannot be implemented in TurboLink yet. 156 | * Do not use [`optional`](https://protobuf.dev/programming-guides/proto3/#specifying-field-rules) field. And I have no plan to support functions like 'has_xxx' or 'clean_xxx' in the blueprint, which will greatly increase the complexity of the generated code. 157 | * Similarly, [`any`](https://protobuf.dev/programming-guides/proto3/#any) message type cannot be used in TurboLink either. 158 | 159 | ## Buy me a coffee 160 | Turbolink is a completely free and open source project. I maintain it in my own free time. If you get help from it, you can consider buying me a cup of coffee. Thank you! 161 | Buy Me A Coffee 162 | -------------------------------------------------------------------------------- /Source/TurboLinkEditor/Private/SGrpcMessageTagWidget.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) Developed by Neo Jin. All Rights Reserved. 2 | #include "SGrpcMessageTagWidget.h" 3 | #include "Widgets/Input/SButton.h" 4 | #include "Widgets/Input/SCheckBox.h" 5 | #include "Widgets/Images/SImage.h" 6 | #include "Widgets/Input/SSearchBox.h" 7 | 8 | #define LOCTEXT_NAMESPACE "GrpcMessageTagWidget" 9 | 10 | void SGrpcMessageTagWidget::Construct(const FArguments& InArgs) 11 | { 12 | OnTagChanged = InArgs._OnTagChanged; 13 | MaxHeight = InArgs._MaxHeight; 14 | SelectedMessageTagName = InArgs._SelectedMessageTagName; 15 | 16 | UGrpcMessageTagsManager& tagsManager = UGrpcMessageTagsManager::Get(); 17 | tagsManager.GetGrpcMessageRootTags(TagItems); 18 | 19 | //is selected tag validity 20 | VerifySelectedTagValidity(); 21 | 22 | ChildSlot 23 | [ 24 | SNew(SBorder) 25 | #if ENGINE_MAJOR_VERSION>=5 26 | .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) 27 | #else 28 | .BorderImage(FEditorStyle::GetBrush("ToolPanel.GroupBorder")) 29 | #endif 30 | [ 31 | SNew(SVerticalBox) 32 | 33 | // Message Tag Tree controls 34 | + SVerticalBox::Slot() 35 | .AutoHeight() 36 | .VAlign(VAlign_Top) 37 | [ 38 | SNew(SHorizontalBox) 39 | 40 | // Expand All nodes 41 | + SHorizontalBox::Slot() 42 | .AutoWidth() 43 | [ 44 | SNew(SButton) 45 | .OnClicked(this, &SGrpcMessageTagWidget::OnExpandAllClicked) 46 | .Text(LOCTEXT("GrpcMessageTagWidget_ExpandAll", "Expand All")) 47 | ] 48 | 49 | // Collapse All nodes 50 | + SHorizontalBox::Slot() 51 | .AutoWidth() 52 | [ 53 | SNew(SButton) 54 | .OnClicked(this, &SGrpcMessageTagWidget::OnCollapseAllClicked) 55 | .Text(LOCTEXT("GrpcMessageTagWidget_CollapseAll", "Collapse All")) 56 | ] 57 | 58 | // Clear selections 59 | + SHorizontalBox::Slot() 60 | .AutoWidth() 61 | [ 62 | SNew(SButton) 63 | .OnClicked(this, &SGrpcMessageTagWidget::OnClearSelectClicked) 64 | .Text(LOCTEXT("GrpcMessageTagWidget_ClearSelect", "Clear Select")) 65 | ] 66 | 67 | // Search 68 | + SHorizontalBox::Slot() 69 | .VAlign(VAlign_Center) 70 | .FillWidth(1.f) 71 | .Padding(5, 1, 5, 1) 72 | [ 73 | SAssignNew(SearchTagBox, SSearchBox) 74 | .HintText(LOCTEXT("GrpcMessageTagWidget_SearchBoxHint", "Search GrpcMessage Tags")) 75 | .OnTextChanged(this, &SGrpcMessageTagWidget::OnFilterTextChanged) 76 | ] 77 | ] 78 | 79 | // Message Tags tree 80 | + SVerticalBox::Slot() 81 | .MaxHeight(MaxHeight) 82 | [ 83 | SAssignNew(TagTreeContainerWidget, SBorder) 84 | .Padding(FMargin(4.f)) 85 | [ 86 | SAssignNew(TagTreeWidget, STreeView< TSharedPtr >) 87 | .TreeItemsSource(&TagItems) 88 | .OnGenerateRow(this, &SGrpcMessageTagWidget::OnGenerateRow) 89 | .OnGetChildren(this, &SGrpcMessageTagWidget::OnGetChildren) 90 | .SelectionMode(ESelectionMode::SingleToggle) 91 | ] 92 | ] 93 | ] 94 | ]; 95 | 96 | FilterTagTree(); 97 | } 98 | 99 | FVector2D SGrpcMessageTagWidget::ComputeDesiredSize(float LayoutScaleMultiplier) const 100 | { 101 | FVector2D WidgetSize = SCompoundWidget::ComputeDesiredSize(LayoutScaleMultiplier); 102 | 103 | FVector2D TagTreeContainerSize = TagTreeContainerWidget->GetDesiredSize(); 104 | 105 | if (TagTreeContainerSize.Y < MaxHeight) 106 | { 107 | WidgetSize.Y += MaxHeight - TagTreeContainerSize.Y; 108 | } 109 | 110 | return WidgetSize; 111 | } 112 | 113 | void SGrpcMessageTagWidget::OnFilterTextChanged(const FText& InFilterText) 114 | { 115 | FilterString = InFilterText.ToString(); 116 | 117 | FilterTagTree(); 118 | } 119 | 120 | void SGrpcMessageTagWidget::FilterTagTree() 121 | { 122 | TagTreeWidget->SetTreeItemsSource(&TagItems); 123 | for (int32 iItem = 0; iItem < TagItems.Num(); ++iItem) 124 | { 125 | SetDefaultTagNodeItemExpansion(TagItems[iItem]); 126 | } 127 | 128 | TagTreeWidget->RequestTreeRefresh(); 129 | } 130 | 131 | void SGrpcMessageTagWidget::SelectMessage(const FName& MessageTagName) 132 | { 133 | SelectedMessageTagName = MessageTagName; 134 | OnTagChanged.ExecuteIfBound(SelectedMessageTagName); 135 | } 136 | 137 | bool SGrpcMessageTagWidget::FilterChildrenCheck(TSharedPtr InItem) 138 | { 139 | if (!InItem.IsValid()) 140 | { 141 | return false; 142 | } 143 | 144 | auto FilterChildrenCheck_r = ([this, InItem]() 145 | { 146 | TArray< TSharedPtr > Children = InItem->GetChildTagNodes(); 147 | for (int32 iChild = 0; iChild < Children.Num(); ++iChild) 148 | { 149 | if (FilterChildrenCheck(Children[iChild])) 150 | { 151 | return true; 152 | } 153 | } 154 | return false; 155 | }); 156 | 157 | if (InItem->TagName.ToString().Contains(FilterString) || FilterString.IsEmpty()) 158 | { 159 | return true; 160 | } 161 | 162 | return FilterChildrenCheck_r(); 163 | } 164 | 165 | TSharedPtr SGrpcMessageTagWidget::GetWidgetToFocusOnOpen() 166 | { 167 | return SearchTagBox; 168 | } 169 | 170 | TSharedRef SGrpcMessageTagWidget::OnGenerateRow(TSharedPtr InItem, const TSharedRef& OwnerTable) 171 | { 172 | FText TooltipText; 173 | bool bWithScriptStruct = false; 174 | if (InItem.IsValid()) 175 | { 176 | FString TooltipString = InItem->TagName.ToString(); 177 | 178 | if (InItem.IsValid() && InItem->MessageScriptStruct != nullptr) 179 | { 180 | bWithScriptStruct = true; 181 | } 182 | TooltipText = FText::FromString(TooltipString); 183 | } 184 | 185 | return SNew(STableRow< TSharedPtr >, OwnerTable) 186 | #if ENGINE_MAJOR_VERSION>=5 187 | .Style(FAppStyle::Get(), "GameplayTagTreeView") 188 | #else 189 | .Style(FEditorStyle::Get(), "GameplayTagTreeView") 190 | #endif 191 | [ 192 | SNew(SHorizontalBox) 193 | 194 | // Tag Selection (selection mode only) 195 | + SHorizontalBox::Slot() 196 | .FillWidth(1.0f) 197 | .HAlign(HAlign_Left) 198 | [ 199 | SNew(SCheckBox) 200 | .OnCheckStateChanged(this, &SGrpcMessageTagWidget::OnTagCheckStatusChanged, InItem) 201 | .IsChecked(this, &SGrpcMessageTagWidget::IsTagChecked, InItem) 202 | .ToolTipText(TooltipText) 203 | .IsEnabled(true) 204 | .Visibility(bWithScriptStruct ? EVisibility::Visible : EVisibility::Collapsed) 205 | [ 206 | SNew(STextBlock) 207 | .Text(FText::FromName(InItem->SimpleTagName)) 208 | ] 209 | ] 210 | 211 | // Normal Tag Display (management mode only) 212 | + SHorizontalBox::Slot() 213 | .FillWidth(1.0f) 214 | .HAlign(HAlign_Left) 215 | [ 216 | SNew(STextBlock) 217 | .ToolTip(FSlateApplication::Get().MakeToolTip(TooltipText)) 218 | .Text(FText::FromName(InItem->SimpleTagName)) 219 | .Visibility(!bWithScriptStruct ? EVisibility::Visible : EVisibility::Collapsed) 220 | ] 221 | ]; 222 | } 223 | 224 | void SGrpcMessageTagWidget::OnGetChildren(TSharedPtr InItem, TArray< TSharedPtr >& OutChildren) 225 | { 226 | TArray< TSharedPtr > FilteredChildren; 227 | TArray< TSharedPtr > Children = InItem->GetChildTagNodes(); 228 | 229 | for (int32 iChild = 0; iChild < Children.Num(); ++iChild) 230 | { 231 | if (FilterChildrenCheck(Children[iChild])) 232 | { 233 | FilteredChildren.Add(Children[iChild]); 234 | } 235 | } 236 | OutChildren += FilteredChildren; 237 | } 238 | 239 | void SGrpcMessageTagWidget::OnTagCheckStatusChanged(ECheckBoxState NewCheckState, TSharedPtr NodeChanged) 240 | { 241 | if (NewCheckState == ECheckBoxState::Checked) 242 | { 243 | SelectMessage(NodeChanged->TagName); 244 | } 245 | } 246 | 247 | ECheckBoxState SGrpcMessageTagWidget::IsTagChecked(TSharedPtr Node) const 248 | { 249 | if (Node->TagName == SelectedMessageTagName) 250 | { 251 | return ECheckBoxState::Checked; 252 | } 253 | return ECheckBoxState::Unchecked;; 254 | } 255 | 256 | bool SGrpcMessageTagWidget::IsChildChecked(TSharedPtr Node) const 257 | { 258 | FString selectedMessageTagName = SelectedMessageTagName.ToString().Left(Node->TagName.GetStringLength()); 259 | if (selectedMessageTagName.Compare(Node->TagName.ToString()) == 0) 260 | { 261 | return true; 262 | } 263 | return false; 264 | } 265 | 266 | FReply SGrpcMessageTagWidget::OnClearSelectClicked() 267 | { 268 | SelectMessage(NAME_None); 269 | return FReply::Handled(); 270 | } 271 | 272 | FReply SGrpcMessageTagWidget::OnExpandAllClicked() 273 | { 274 | SetTagTreeItemExpansion(true); 275 | return FReply::Handled(); 276 | } 277 | 278 | FReply SGrpcMessageTagWidget::OnCollapseAllClicked() 279 | { 280 | SetTagTreeItemExpansion(false); 281 | return FReply::Handled(); 282 | } 283 | 284 | void SGrpcMessageTagWidget::SetTagTreeItemExpansion(bool bExpand) 285 | { 286 | for (int32 TagIdx = 0; TagIdx < TagItems.Num(); ++TagIdx) 287 | { 288 | SetTagNodeItemExpansion(TagItems[TagIdx], bExpand); 289 | } 290 | } 291 | 292 | void SGrpcMessageTagWidget::SetTagNodeItemExpansion(TSharedPtr Node, bool bExpand) 293 | { 294 | if (Node.IsValid() && TagTreeWidget.IsValid()) 295 | { 296 | TagTreeWidget->SetItemExpansion(Node, bExpand); 297 | 298 | const TArray< TSharedPtr >& ChildTags = Node->GetChildTagNodes(); 299 | for (int32 ChildIdx = 0; ChildIdx < ChildTags.Num(); ++ChildIdx) 300 | { 301 | SetTagNodeItemExpansion(ChildTags[ChildIdx], bExpand); 302 | } 303 | } 304 | } 305 | 306 | void SGrpcMessageTagWidget::SetDefaultTagNodeItemExpansion(TSharedPtr Node) 307 | { 308 | if (Node.IsValid() && TagTreeWidget.IsValid()) 309 | { 310 | bool bExpanded = false; 311 | if (!FilterString.IsEmpty()) 312 | { 313 | bExpanded = true; 314 | } 315 | 316 | if (IsTagChecked(Node) == ECheckBoxState::Checked || IsChildChecked(Node)) 317 | { 318 | bExpanded = true; 319 | } 320 | TagTreeWidget->SetItemExpansion(Node, bExpanded); 321 | 322 | const TArray< TSharedPtr >& ChildTags = Node->GetChildTagNodes(); 323 | for (int32 ChildIdx = 0; ChildIdx < ChildTags.Num(); ++ChildIdx) 324 | { 325 | SetDefaultTagNodeItemExpansion(ChildTags[ChildIdx]); 326 | } 327 | } 328 | } 329 | 330 | void SGrpcMessageTagWidget::VerifySelectedTagValidity() 331 | { 332 | if (SelectedMessageTagName == NAME_None) return; 333 | 334 | UGrpcMessageTagsManager& tagsManager = UGrpcMessageTagsManager::Get(); 335 | TSharedPtr messageTag = tagsManager.FindGrpcMessageTag(SelectedMessageTagName, true); 336 | if (messageTag.IsValid()) return; 337 | 338 | //clean selected 339 | SelectMessage(NAME_None); 340 | } 341 | 342 | #undef LOCTEXT_NAMESPACE 343 | -------------------------------------------------------------------------------- /Source/TurboLinkEditor/Private/GrpcMessageK2Node_JsonToMessage.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) Developed by Neo Jin. All Rights Reserved. 2 | #include "GrpcMessageK2Node_JsonToMessage.h" 3 | #include "EdGraphSchema_K2.h" 4 | #include "BlueprintNodeSpawner.h" 5 | #include "BlueprintActionDatabaseRegistrar.h" 6 | #include "K2Node_CallFunction.h" 7 | #include "KismetCompiler.h" 8 | #include "Kismet2/BlueprintEditorUtils.h" 9 | #include "TurboLinkGrpcMessage.h" 10 | #include "TurboLinkGrpcUtilities.h" 11 | #include "GrpcMessageTagsManager.h" 12 | 13 | #define LOCTEXT_NAMESPACE "K2Node_JsonToGrpcMessageNode" 14 | 15 | struct FK2Node_JsonToGrpcMessageHelper 16 | { 17 | static FName MessageTypePinName; 18 | static FName JsonStringPinName; 19 | static FName ReturnMessagePinName; 20 | }; 21 | 22 | FName FK2Node_JsonToGrpcMessageHelper::MessageTypePinName(TEXT("MessageType")); 23 | FName FK2Node_JsonToGrpcMessageHelper::JsonStringPinName(TEXT("JsonString")); 24 | FName FK2Node_JsonToGrpcMessageHelper::ReturnMessagePinName(TEXT("ReturnMessage")); 25 | 26 | void UJsonToGrpcMessageNode::GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const 27 | { 28 | UClass* ActionKey = GetClass(); 29 | if (ActionRegistrar.IsOpenForRegistration(ActionKey)) 30 | { 31 | UBlueprintNodeSpawner* NodeSpawner = UBlueprintNodeSpawner::Create(GetClass()); 32 | check(NodeSpawner != nullptr); 33 | 34 | ActionRegistrar.AddBlueprintAction(ActionKey, NodeSpawner); 35 | } 36 | } 37 | 38 | FText UJsonToGrpcMessageNode::GetMenuCategory() const 39 | { 40 | return LOCTEXT("MenuCategory", "TurboLink"); 41 | } 42 | 43 | FText UJsonToGrpcMessageNode::GetNodeTitle(ENodeTitleType::Type Title) const 44 | { 45 | return LOCTEXT("DefaultTitle", "Json String To GrpcMessage"); 46 | } 47 | 48 | FText UJsonToGrpcMessageNode::GetTooltipText() const 49 | { 50 | return FText::FromString(TEXT("Convert Json String to GrpcMessage")); 51 | } 52 | 53 | void UJsonToGrpcMessageNode::GetPinHoverText(const UEdGraphPin& Pin, FString& HoverTextOut) const 54 | { 55 | if (Pin.PinName == FK2Node_JsonToGrpcMessageHelper::ReturnMessagePinName) 56 | { 57 | //set friend message name 58 | UScriptStruct* scriptStruct = GetMessageScriptStruct(); 59 | if (scriptStruct != nullptr) 60 | { 61 | HoverTextOut = FString::Printf(TEXT("Return Message\n%s"), *(scriptStruct->GetFName().ToString())); 62 | return; 63 | } 64 | } 65 | Super::GetPinHoverText(Pin, HoverTextOut); 66 | } 67 | 68 | void UJsonToGrpcMessageNode::AllocateDefaultPins() 69 | { 70 | // Add execution pins 71 | UEdGraphPin* execPin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Execute); 72 | UEdGraphPin* thenPin = CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Then); 73 | 74 | // Add Message type pin 75 | UEdGraphPin* messageTypePin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Struct, FGrpcMessageTag::StaticStruct(), FK2Node_JsonToGrpcMessageHelper::MessageTypePinName); 76 | messageTypePin->bNotConnectable = 1; 77 | 78 | // Add String pin 79 | UEdGraphPin* jsonStringPin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_String, FK2Node_JsonToGrpcMessageHelper::JsonStringPinName); 80 | 81 | // Add return message pin 82 | UEdGraphPin* returnMessagePin = CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Struct, FGrpcMessage::StaticStruct(), FK2Node_JsonToGrpcMessageHelper::ReturnMessagePinName); 83 | 84 | // Add result pin 85 | UEdGraphPin* returnValuePin = CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Boolean, UEdGraphSchema_K2::PN_ReturnValue); 86 | returnValuePin->PinFriendlyName = FText::FromString(TEXT("IsSuccess")); 87 | 88 | Super::AllocateDefaultPins(); 89 | } 90 | 91 | void UJsonToGrpcMessageNode::PinConnectionListChanged(UEdGraphPin* Pin) 92 | { 93 | Super::PinConnectionListChanged(Pin); 94 | 95 | if (Pin && (Pin->PinName == FK2Node_JsonToGrpcMessageHelper::MessageTypePinName)) 96 | { 97 | OnMessageTypeChanged(); 98 | } 99 | } 100 | 101 | void UJsonToGrpcMessageNode::PinDefaultValueChanged(UEdGraphPin* ChangedPin) 102 | { 103 | if (ChangedPin && (ChangedPin->PinName == FK2Node_JsonToGrpcMessageHelper::MessageTypePinName)) 104 | { 105 | OnMessageTypeChanged(); 106 | } 107 | } 108 | 109 | void UJsonToGrpcMessageNode::ReallocatePinsDuringReconstruction(TArray& OldPins) 110 | { 111 | AllocateDefaultPins(); 112 | 113 | // Change class of output pin 114 | UEdGraphPin* returnValuePin = GetReturnMessagePin(); 115 | returnValuePin->PinType.PinSubCategoryObject = GetMessageScriptStruct(&OldPins); 116 | 117 | RestoreSplitPins(OldPins); 118 | } 119 | 120 | void UJsonToGrpcMessageNode::OnMessageTypeChanged() 121 | { 122 | UEdGraphPin* returnMessagePin = GetReturnMessagePin(); 123 | 124 | // Change class of output pin 125 | returnMessagePin->PinType.PinSubCategoryObject = GetMessageScriptStruct(); 126 | 127 | // Cache all the pin connections to the ResultPin, we will attempt to recreate them 128 | TArray resultPinConnectionList = returnMessagePin->LinkedTo; 129 | // Because the archetype has changed, we break the output link as the output pin type will change 130 | returnMessagePin->BreakAllPinLinks(true); 131 | 132 | // Recreate any pin links to the Result pin that are still valid 133 | const UEdGraphSchema_K2* K2Schema = GetDefault(); 134 | for (UEdGraphPin* connections : resultPinConnectionList) 135 | { 136 | K2Schema->TryCreateConnection(returnMessagePin, connections); 137 | } 138 | 139 | // Refresh the UI for the graph so the pin changes show up 140 | GetGraph()->NotifyGraphChanged(); 141 | 142 | // Mark dirty 143 | FBlueprintEditorUtils::MarkBlueprintAsModified(GetBlueprint()); 144 | } 145 | 146 | UScriptStruct* UJsonToGrpcMessageNode::GetMessageScriptStruct(const TArray* InPinsToSearch) const 147 | { 148 | UScriptStruct* returnStruct = nullptr; 149 | UEdGraphPin* inputPin = GetInputPin(FK2Node_JsonToGrpcMessageHelper::MessageTypePinName, InPinsToSearch); 150 | if (inputPin == nullptr) return nullptr; 151 | 152 | //find message tag 153 | FGrpcMessageTag tempMessageTag; 154 | tempMessageTag.FromExportString(inputPin->DefaultValue, PPF_SerializedAsImportText); 155 | 156 | TSharedPtr grpcMessageTag = UGrpcMessageTagsManager::Get().FindGrpcMessageTag(tempMessageTag.TagName, true); 157 | if (grpcMessageTag.IsValid()) 158 | { 159 | returnStruct = grpcMessageTag->MessageScriptStruct; 160 | } 161 | return returnStruct; 162 | } 163 | 164 | UEdGraphPin* UJsonToGrpcMessageNode::GetThenPin() const 165 | { 166 | UEdGraphPin* Pin = FindPinChecked(UEdGraphSchema_K2::PN_Then); 167 | check(Pin->Direction == EGPD_Output); 168 | return Pin; 169 | } 170 | 171 | UEdGraphPin* UJsonToGrpcMessageNode::GetInputPin(const FName& PinName, const TArray* InPinsToSearch) const 172 | { 173 | const TArray* PinsToSearch = InPinsToSearch ? InPinsToSearch : &Pins; 174 | 175 | UEdGraphPin* Pin = nullptr; 176 | for (UEdGraphPin* TestPin : *PinsToSearch) 177 | { 178 | if (TestPin && TestPin->PinName == PinName) 179 | { 180 | Pin = TestPin; 181 | break; 182 | } 183 | } 184 | check(Pin == nullptr || Pin->Direction == EGPD_Input); 185 | return Pin; 186 | } 187 | 188 | UEdGraphPin* UJsonToGrpcMessageNode::GetReturnMessagePin(const TArray* InPinsToSearch) const 189 | { 190 | const TArray* PinsToSearch = InPinsToSearch ? InPinsToSearch : &Pins; 191 | 192 | UEdGraphPin* Pin = nullptr; 193 | for (UEdGraphPin* TestPin : *PinsToSearch) 194 | { 195 | if (TestPin && TestPin->PinName == FK2Node_JsonToGrpcMessageHelper::ReturnMessagePinName) 196 | { 197 | Pin = TestPin; 198 | break; 199 | } 200 | } 201 | check(Pin == nullptr || Pin->Direction == EGPD_Output); 202 | return Pin; 203 | } 204 | 205 | UEdGraphPin* UJsonToGrpcMessageNode::GetResultPin() const 206 | { 207 | UEdGraphPin* Pin = FindPin(UEdGraphSchema_K2::PN_ReturnValue, EGPD_Output); 208 | return Pin; 209 | } 210 | 211 | void UJsonToGrpcMessageNode::ExpandNode(FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph) 212 | { 213 | Super::ExpandNode(CompilerContext, SourceGraph); 214 | 215 | UEdGraphPin* execPin = GetExecPin(); 216 | UEdGraphPin* jsonStringPin = GetInputPin(FK2Node_JsonToGrpcMessageHelper::JsonStringPinName); 217 | UEdGraphPin* returnMessagePin = GetReturnMessagePin(); 218 | UEdGraphPin* returnValuePin = GetResultPin(); 219 | UEdGraphPin* thenPin = GetThenPin(); 220 | 221 | //valid message type assigned? 222 | UScriptStruct* structType = GetMessageScriptStruct(); 223 | if (structType == nullptr) 224 | { 225 | //compile error 226 | CompilerContext.MessageLog.Error(TEXT("Must assign a valid grpc message type. @@"), this); 227 | BreakAllNodeLinks(); 228 | return; 229 | } 230 | 231 | // not working yet! 232 | if (execPin == nullptr || thenPin == nullptr) 233 | { 234 | BreakAllNodeLinks(); 235 | return; 236 | } 237 | 238 | // here we adapt/bind our pins to the static function pins that we are calling. 239 | UK2Node_CallFunction* callFunctionNode = CompilerContext.SpawnIntermediateNode(this, SourceGraph); 240 | static FName InternalFunctionName = GET_FUNCTION_NAME_CHECKED(UGrpcMessageToJsonFunctionLibrary, JsonToGrpcMessageInternal); 241 | callFunctionNode->FunctionReference.SetExternalMember(InternalFunctionName, UGrpcMessageToJsonFunctionLibrary::StaticClass()); 242 | callFunctionNode->AllocateDefaultPins(); 243 | 244 | // get pins from call function node 245 | UEdGraphPin* callFunction_ExecPin = callFunctionNode->GetExecPin(); 246 | UEdGraphPin* callFunction_JsonStringPin = callFunctionNode->FindPinChecked(FK2Node_JsonToGrpcMessageHelper::JsonStringPinName); 247 | UEdGraphPin* callFunction_ReturnMessagePin = callFunctionNode->FindPinChecked(FK2Node_JsonToGrpcMessageHelper::ReturnMessagePinName); 248 | UEdGraphPin* callFunction_ReturnValuePin = callFunctionNode->GetReturnValuePin(); 249 | UEdGraphPin* callFunction_ThenPin = callFunctionNode->GetThenPin(); 250 | 251 | //json string pin 252 | CompilerContext.MovePinLinksToIntermediate(*jsonStringPin, *callFunction_JsonStringPin); 253 | //return message pin 254 | CompilerContext.MovePinLinksToIntermediate(*returnMessagePin, *callFunction_ReturnMessagePin); 255 | callFunction_ReturnMessagePin->PinType = returnMessagePin->PinType; 256 | //return value pin 257 | CompilerContext.MovePinLinksToIntermediate(*returnValuePin, *callFunction_ReturnValuePin); 258 | //exec pin 259 | CompilerContext.MovePinLinksToIntermediate(*execPin, *callFunction_ExecPin); 260 | //then pin 261 | CompilerContext.MovePinLinksToIntermediate(*thenPin, *callFunction_ThenPin); 262 | // break any links to the expanded node 263 | BreakAllNodeLinks(); 264 | } 265 | 266 | #undef LOCTEXT_NAMESPACE 267 | -------------------------------------------------------------------------------- /Source/TurboLinkGrpc/Private/TurboLinkGrpcContext.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) Developed by Neo Jin. All Rights Reserved. 2 | #pragma once 3 | 4 | #include "CoreMinimal.h" 5 | #include "TurboLinkGrpcClient.h" 6 | #include "TurboLinkGrpcModule.h" 7 | #include "grpcpp/impl/codegen/async_unary_call.h" 8 | #include 9 | 10 | class UGrpcService; 11 | class UGrpcClient; 12 | 13 | class GrpcContext : public TSharedFromThis 14 | { 15 | friend class UTurboLinkGrpcManager; 16 | friend class UGrpcClient; 17 | public: 18 | FGrpcContextHandle GetHandle() const 19 | { 20 | return Handle; 21 | } 22 | 23 | EGrpcContextState GetState() const 24 | { 25 | return ContextState; 26 | } 27 | 28 | void TryCancel(); 29 | 30 | public: 31 | UGrpcService* const Service; 32 | UGrpcClient* const Client; 33 | 34 | // Context for the client. It could be used to convey extra information to the server and/or tweak certain RPC behaviors. 35 | std::unique_ptr RpcContext; 36 | // Storage for the status of the RPC upon completion. 37 | grpc::Status RpcStatus; 38 | 39 | // Convert grpc status code to EGrpcResultCode 40 | static EGrpcResultCode ConvertStatusCode(const grpc::Status& RpcStatus); 41 | 42 | // Build grpc Result 43 | static FGrpcResult MakeGrpcResult(const grpc::Status& RpcStatus); 44 | 45 | //async tag 46 | void* InitialTag = nullptr; 47 | void* WriteTag = nullptr; 48 | void* ReadTag = nullptr; 49 | 50 | protected: 51 | void UpdateState(EGrpcContextState NewState); 52 | virtual void OnRpcEvent(bool Ok, const void* EventTag) = 0; 53 | 54 | protected: 55 | FGrpcContextHandle Handle; 56 | EGrpcContextState ContextState; 57 | 58 | public: 59 | GrpcContext(FGrpcContextHandle _Handle, UGrpcService* _Service, UGrpcClient* _Client); 60 | virtual ~GrpcContext(); 61 | }; 62 | 63 | template 64 | class TGrpcContext : public GrpcContext 65 | { 66 | public: 67 | typedef std::function FRpcCallbackFunc; 68 | 69 | //Reader and Writer 70 | std::unique_ptr RpcReaderWriter; 71 | //Response Message 72 | R RpcResponse; 73 | 74 | public: 75 | TGrpcContext(FGrpcContextHandle _Handle, UGrpcService* _Service, UGrpcClient* _Client) 76 | : GrpcContext(_Handle, _Service, _Client) 77 | { 78 | } 79 | }; 80 | 81 | template 82 | class GrpcContext_Ping_Pong : public TGrpcContext 83 | { 84 | typedef TGrpcContext Super; 85 | 86 | protected: 87 | void OnRpcEventInternal(bool Ok, const void* EventTag, typename Super::FRpcCallbackFunc RpcCallbackFunc) 88 | { 89 | if (!Ok) 90 | { 91 | Super::RpcReaderWriter->Finish(&(Super::RpcResponse), &(Super::RpcStatus), Super::ReadTag); 92 | return; 93 | } 94 | 95 | FGrpcResult result = GrpcContext::MakeGrpcResult(Super::RpcStatus); 96 | if (Super::RpcStatus.ok()) 97 | { 98 | if (Super::GetState() == EGrpcContextState::Initialing) 99 | { 100 | check(EventTag == Super::InitialTag); 101 | 102 | Super::RpcReaderWriter->Finish(&(Super::RpcResponse), &(Super::RpcStatus), Super::ReadTag); 103 | Super::UpdateState(EGrpcContextState::Busy); 104 | } 105 | else 106 | { 107 | if (RpcCallbackFunc) 108 | { 109 | RpcCallbackFunc(result, &(Super::RpcResponse)); 110 | } 111 | Super::UpdateState(EGrpcContextState::Done); 112 | return; 113 | } 114 | } 115 | else 116 | { 117 | UE_LOG(LogTurboLink, Error, TEXT("CallRpcError: %s"), *result.GetMessageString()); 118 | 119 | if (RpcCallbackFunc) 120 | { 121 | RpcCallbackFunc(result, nullptr); 122 | } 123 | Super::UpdateState(EGrpcContextState::Done); 124 | return; 125 | } 126 | } 127 | 128 | public: 129 | GrpcContext_Ping_Pong(FGrpcContextHandle _Handle, UGrpcService* _Service, UGrpcClient* _Client) 130 | : Super(_Handle, _Service, _Client) 131 | { 132 | } 133 | }; 134 | 135 | template 136 | class GrpcContext_Ping_Stream : public TGrpcContext 137 | { 138 | typedef TGrpcContext Super; 139 | protected: 140 | void OnRpcEventInternal(bool Ok, const void* EventTag, typename Super::FRpcCallbackFunc RpcCallbackFunc) 141 | { 142 | if (!Ok) 143 | { 144 | Super::RpcReaderWriter->Finish(&(Super::RpcStatus), Super::ReadTag); 145 | Super::UpdateState(EGrpcContextState::Done); 146 | return; 147 | } 148 | 149 | FGrpcResult result = GrpcContext::MakeGrpcResult(Super::RpcStatus); 150 | if (Super::RpcStatus.ok()) 151 | { 152 | if (Super::GetState() == EGrpcContextState::Initialing) 153 | { 154 | check(EventTag == Super::InitialTag); 155 | 156 | Super::RpcReaderWriter->Read(&(Super::RpcResponse), Super::ReadTag); 157 | Super::UpdateState(EGrpcContextState::Busy); 158 | } 159 | else 160 | { 161 | if (RpcCallbackFunc) 162 | { 163 | RpcCallbackFunc(result, &(Super::RpcResponse)); 164 | } 165 | Super::RpcReaderWriter->Read(&(Super::RpcResponse), Super::ReadTag); 166 | } 167 | } 168 | else 169 | { 170 | UE_LOG(LogTurboLink, Error, TEXT("CallRpcError: %s"), *result.GetMessageString()); 171 | 172 | if (RpcCallbackFunc) 173 | { 174 | RpcCallbackFunc(result, nullptr); 175 | } 176 | Super::UpdateState(EGrpcContextState::Done); 177 | return; 178 | } 179 | } 180 | public: 181 | GrpcContext_Ping_Stream(FGrpcContextHandle _Handle, UGrpcService* _Service, UGrpcClient* _Client) 182 | : Super(_Handle, _Service, _Client) 183 | { 184 | } 185 | }; 186 | 187 | template 188 | class GrpcContext_Stream_Pong : public TGrpcContext 189 | { 190 | typedef TGrpcContext Super; 191 | typedef std::function FSendCompleteCallbackFunc; 192 | 193 | protected: 194 | std::vector SendQueue; 195 | bool bCanSend = false; 196 | bool bWritesDone = false; 197 | 198 | protected: 199 | void OnRpcEventInternal(bool Ok, const void* EventTag, 200 | typename Super::FRpcCallbackFunc RpcCallbackFunc, FSendCompleteCallbackFunc SendCompleteCallbackFunc) 201 | { 202 | if (!Ok) 203 | { 204 | Super::RpcReaderWriter->Finish(&(Super::RpcStatus), Super::ReadTag); 205 | return; 206 | } 207 | 208 | FGrpcResult result = GrpcContext::MakeGrpcResult(Super::RpcStatus); 209 | if (Super::RpcStatus.ok()) 210 | { 211 | if (Super::GetState() == EGrpcContextState::Initialing) 212 | { 213 | check(EventTag == Super::InitialTag); 214 | 215 | if (SendQueue.empty()) 216 | { 217 | bCanSend = true; 218 | } 219 | else 220 | { 221 | Super::RpcReaderWriter->Write(SendQueue.front(), Super::WriteTag); 222 | SendQueue.erase(SendQueue.begin()); 223 | } 224 | 225 | // Register a handler to be called when the server has sent a reply and final status. 226 | Super::RpcReaderWriter->Finish(&(Super::RpcStatus), Super::ReadTag); 227 | Super::UpdateState(EGrpcContextState::Busy); 228 | } 229 | else 230 | { 231 | if (EventTag == Super::ReadTag) 232 | { 233 | if (RpcCallbackFunc) 234 | { 235 | RpcCallbackFunc(result, &(Super::RpcResponse)); 236 | } 237 | Super::UpdateState(EGrpcContextState::Done); 238 | } 239 | else if (EventTag == Super::WriteTag) 240 | { 241 | if (SendQueue.empty()) 242 | { 243 | bCanSend = true; 244 | if (SendCompleteCallbackFunc && !bWritesDone) 245 | { 246 | SendCompleteCallbackFunc(); 247 | } 248 | } 249 | else 250 | { 251 | if (!bWritesDone) 252 | { 253 | Super::RpcReaderWriter->Write(SendQueue.front(), Super::WriteTag); 254 | SendQueue.erase(SendQueue.begin()); 255 | } 256 | } 257 | } 258 | } 259 | } 260 | else 261 | { 262 | UE_LOG(LogTurboLink, Error, TEXT("CallRpcError: %s"), *result.GetMessageString()); 263 | 264 | if (RpcCallbackFunc) 265 | { 266 | RpcCallbackFunc(result, nullptr); 267 | } 268 | Super::UpdateState(EGrpcContextState::Done); 269 | return; 270 | } 271 | } 272 | 273 | public: 274 | GrpcContext_Stream_Pong(FGrpcContextHandle _Handle, UGrpcService* _Service, UGrpcClient* _Client) 275 | : Super(_Handle, _Service, _Client) 276 | { 277 | } 278 | }; 279 | 280 | template 281 | class GrpcContext_Stream_Stream : public TGrpcContext 282 | { 283 | typedef TGrpcContext Super; 284 | typedef std::function FSendCompleteCallbackFunc; 285 | 286 | protected: 287 | std::vector SendQueue; 288 | bool bCanSend = false; 289 | 290 | protected: 291 | void OnRpcEventInternal(bool Ok, const void* EventTag, 292 | typename Super::FRpcCallbackFunc RpcCallbackFunc, FSendCompleteCallbackFunc SendCompleteCallbackFunc) 293 | { 294 | if (!Ok) 295 | { 296 | Super::RpcReaderWriter->Finish(&(Super::RpcStatus), Super::ReadTag); 297 | return; 298 | } 299 | 300 | FGrpcResult result = GrpcContext::MakeGrpcResult(Super::RpcStatus); 301 | if (Super::RpcStatus.ok()) 302 | { 303 | if (Super::GetState() == EGrpcContextState::Initialing) 304 | { 305 | check(EventTag == Super::InitialTag); 306 | 307 | if (SendQueue.empty()) 308 | { 309 | bCanSend = true; 310 | } 311 | else 312 | { 313 | Super::RpcReaderWriter->Write(SendQueue.front(), Super::WriteTag); 314 | SendQueue.erase(SendQueue.begin()); 315 | } 316 | Super::RpcReaderWriter->Read(&(Super::RpcResponse), Super::ReadTag); 317 | Super::UpdateState(EGrpcContextState::Busy); 318 | } 319 | else 320 | { 321 | if (EventTag == Super::ReadTag) 322 | { 323 | if (RpcCallbackFunc) 324 | { 325 | RpcCallbackFunc(result, &(Super::RpcResponse)); 326 | } 327 | Super::RpcReaderWriter->Read(&(Super::RpcResponse), Super::ReadTag); 328 | } 329 | else if (EventTag == Super::WriteTag) 330 | { 331 | if (SendQueue.empty()) 332 | { 333 | bCanSend = true; 334 | if (SendCompleteCallbackFunc) 335 | { 336 | SendCompleteCallbackFunc(); 337 | } 338 | } 339 | else 340 | { 341 | Super::RpcReaderWriter->Write(SendQueue.front(), Super::WriteTag); 342 | SendQueue.erase(SendQueue.begin()); 343 | } 344 | } 345 | } 346 | } 347 | else 348 | { 349 | UE_LOG(LogTurboLink, Error, TEXT("CallRpcError: %s"), *result.GetMessageString()); 350 | 351 | if (RpcCallbackFunc) 352 | { 353 | RpcCallbackFunc(result, &(Super::RpcResponse)); 354 | } 355 | Super::UpdateState(EGrpcContextState::Done); 356 | return; 357 | } 358 | } 359 | 360 | public: 361 | GrpcContext_Stream_Stream(FGrpcContextHandle _Handle, UGrpcService* _Service, UGrpcClient* _Client) 362 | : Super(_Handle, _Service, _Client) 363 | { 364 | } 365 | }; 366 | -------------------------------------------------------------------------------- /Source/TurboLinkGrpc/TurboLinkGrpc.Build.cs: -------------------------------------------------------------------------------- 1 | 2 | using UnrealBuildTool; 3 | using System; 4 | using System.IO; 5 | using System.Collections.Generic; 6 | using System.Reflection; 7 | 8 | public class TurboLinkGrpc : ModuleRules 9 | { 10 | private static TurboLinkPlatform TurboLinkPlatformInstance; 11 | 12 | public TurboLinkGrpc(ReadOnlyTargetRules Target) : base(Target) 13 | { 14 | PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; 15 | TurboLinkPlatformInstance = GetTurboLinkPlatformInstance(Target); 16 | 17 | PublicDependencyModuleNames.AddRange( 18 | new string[] 19 | { 20 | "Core" 21 | } 22 | ); 23 | 24 | PrivateDependencyModuleNames.AddRange( 25 | new string[] 26 | { 27 | "CoreUObject", 28 | "Engine", 29 | "Slate", 30 | "SlateCore", 31 | "Serialization", 32 | "Networking", 33 | "DeveloperSettings" 34 | } 35 | ); 36 | AddEngineThirdPartyPrivateStaticDependencies(Target, "OpenSSL"); 37 | AddEngineThirdPartyPrivateStaticDependencies(Target, "zlib"); 38 | 39 | PrivateIncludePaths.Add(Path.Combine(ModuleDirectory, "Private/pb")); 40 | 41 | //ThirdParty include 42 | PrivateIncludePaths.AddRange( 43 | new string[] { 44 | Path.Combine(ThirdPartyRoot(), "protobuf/include"), 45 | Path.Combine(ThirdPartyRoot(), "grpc/include"), 46 | Path.Combine(ThirdPartyRoot(), "re2/include"), 47 | Path.Combine(ThirdPartyRoot(), "abseil/include") 48 | } 49 | ); 50 | 51 | //ThirdParty Libraries 52 | ConfigurePlatform(Target.Platform.ToString(), Target.Configuration); 53 | } 54 | 55 | private TurboLinkPlatform GetTurboLinkPlatformInstance(ReadOnlyTargetRules Target) 56 | { 57 | var TurboLinkPlatformType = System.Type.GetType("TurboLinkPlatform_" + Target.Platform.ToString()); 58 | if (TurboLinkPlatformType == null) 59 | { 60 | throw new BuildException("TurboLink does not support platform " + Target.Platform.ToString()); 61 | } 62 | 63 | var PlatformInstance = Activator.CreateInstance(TurboLinkPlatformType) as TurboLinkPlatform; 64 | if (PlatformInstance == null) 65 | { 66 | throw new BuildException("TurboLink could not instantiate platform " + Target.Platform.ToString()); 67 | } 68 | 69 | return PlatformInstance; 70 | } 71 | 72 | protected string ThirdPartyRoot() 73 | { 74 | return Path.GetFullPath(Path.Combine(ModuleDirectory, "../ThirdParty/")); 75 | } 76 | 77 | private List GrpcLibs = new List 78 | { 79 | "grpc", "grpc++", "gpr", "upb", "address_sorting" 80 | }; 81 | private List ProtobufLibs = new List 82 | { 83 | "protobuf", "utf8_validity", "utf8_range" 84 | }; 85 | private List AbseilLibs = new List 86 | { 87 | "absl_log_internal_check_op", "absl_leak_check", "absl_die_if_null", 88 | "absl_log_internal_conditions", "absl_log_internal_message", 89 | "absl_log_internal_nullguard", "absl_examine_stack", "absl_log_internal_format", 90 | "absl_log_internal_proto", "absl_log_internal_log_sink_set", "absl_log_sink", 91 | "absl_log_entry", "absl_log_initialize", "absl_log_globals", 92 | "absl_log_internal_globals", "absl_statusor", "absl_flags", "absl_flags_internal", 93 | "absl_flags_reflection", "absl_hash", "absl_city", "absl_low_level_hash", 94 | "absl_raw_hash_set", "absl_hashtablez_sampler", "absl_flags_config", 95 | "absl_flags_program_name", "absl_flags_private_handle_accessor", 96 | "absl_flags_commandlineflag", "absl_flags_commandlineflag_internal", 97 | "absl_status", "absl_cord", "absl_cordz_info", "absl_cord_internal", 98 | "absl_cordz_functions", "absl_exponential_biased", "absl_cordz_handle", 99 | "absl_crc_cord_state", "absl_crc32c", "absl_crc_internal", "absl_crc_cpu_detect", 100 | "absl_strerror", "absl_synchronization", "absl_stacktrace", "absl_symbolize", 101 | "absl_debugging_internal", "absl_demangle_internal", "absl_graphcycles_internal", 102 | "absl_malloc_internal", "absl_time", "absl_civil_time", "absl_time_zone", 103 | "absl_bad_variant_access", "absl_flags_marshalling", "absl_str_format_internal", 104 | "absl_random_distributions", "absl_random_seed_sequences", "absl_random_internal_pool_urbg", 105 | "absl_random_internal_randen", "absl_random_internal_randen_hwaes", 106 | "absl_random_internal_randen_hwaes_impl", "absl_random_internal_randen_slow", 107 | "absl_random_internal_platform", "absl_random_internal_seed_material", 108 | "absl_bad_optional_access", "absl_strings", "absl_throw_delegate", "absl_int128", 109 | "absl_strings_internal", "absl_base", "absl_spinlock_wait", "absl_raw_logging_internal", 110 | "absl_log_severity", "absl_random_seed_gen_exception" 111 | }; 112 | private List Re2Libs = new List 113 | { 114 | "re2" 115 | }; 116 | 117 | private bool ConfigurePlatform(string Platform, UnrealTargetConfiguration Configuration) 118 | { 119 | //turbolink thirdparty libraries root path 120 | string root = ThirdPartyRoot(); 121 | 122 | //grpc 123 | foreach(var lib in GrpcLibs) 124 | { 125 | foreach(var arch in TurboLinkPlatformInstance.Architectures()) 126 | { 127 | string fullPath = root + "grpc/" + "lib/" + 128 | TurboLinkPlatformInstance.LibrariesPath + arch + 129 | TurboLinkPlatformInstance.ConfigurationDir(Configuration) + 130 | TurboLinkPlatformInstance.LibraryPrefixName + lib + TurboLinkPlatformInstance.LibraryPostfixName; 131 | PublicAdditionalLibraries.Add(fullPath); 132 | } 133 | } 134 | //protobuf 135 | foreach(var lib in ProtobufLibs) 136 | { 137 | foreach (var arch in TurboLinkPlatformInstance.Architectures()) 138 | { 139 | string libPrefixName = TurboLinkPlatformInstance.LibraryPrefixName; 140 | if (TurboLinkPlatformInstance is TurboLinkPlatform_Win64 && lib == "protobuf") 141 | { 142 | libPrefixName = "lib"; //'libprotobuf.lib' 143 | } 144 | 145 | string fullPath = root + "protobuf/" + "lib/" + 146 | TurboLinkPlatformInstance.LibrariesPath + arch + 147 | TurboLinkPlatformInstance.ConfigurationDir(Configuration) + 148 | libPrefixName + lib + TurboLinkPlatformInstance.LibraryPostfixName; 149 | PublicAdditionalLibraries.Add(fullPath); 150 | } 151 | } 152 | //abseil 153 | foreach (var lib in AbseilLibs) 154 | { 155 | foreach (var arch in TurboLinkPlatformInstance.Architectures()) 156 | { 157 | string fullPath = root + "abseil/" + "lib/" + 158 | TurboLinkPlatformInstance.LibrariesPath + arch + 159 | TurboLinkPlatformInstance.ConfigurationDir(Configuration) + 160 | TurboLinkPlatformInstance.LibraryPrefixName + lib + TurboLinkPlatformInstance.LibraryPostfixName; 161 | PublicAdditionalLibraries.Add(fullPath); 162 | } 163 | } 164 | //re2 165 | foreach (var lib in Re2Libs) 166 | { 167 | foreach (var arch in TurboLinkPlatformInstance.Architectures()) 168 | { 169 | string fullPath = root + "re2/" + "lib/" + 170 | TurboLinkPlatformInstance.LibrariesPath + arch + 171 | TurboLinkPlatformInstance.ConfigurationDir(Configuration) + 172 | TurboLinkPlatformInstance.LibraryPrefixName + lib + TurboLinkPlatformInstance.LibraryPostfixName; 173 | PublicAdditionalLibraries.Add(fullPath); 174 | } 175 | } 176 | return false; 177 | } 178 | } 179 | 180 | public abstract class TurboLinkPlatform 181 | { 182 | public virtual string ConfigurationDir(UnrealTargetConfiguration Configuration) 183 | { 184 | if (Configuration == UnrealTargetConfiguration.Debug || Configuration == UnrealTargetConfiguration.DebugGame) 185 | { 186 | return "Debug/"; 187 | } 188 | else 189 | { 190 | return "Release/"; 191 | } 192 | } 193 | public abstract string LibrariesPath { get; } 194 | public abstract List Architectures(); 195 | public abstract string LibraryPrefixName { get; } 196 | public abstract string LibraryPostfixName { get; } 197 | } 198 | 199 | public class TurboLinkPlatform_Win64 : TurboLinkPlatform 200 | { 201 | public override string ConfigurationDir(UnrealTargetConfiguration Configuration) 202 | { 203 | if (Configuration == UnrealTargetConfiguration.Debug || Configuration == UnrealTargetConfiguration.DebugGame) 204 | { 205 | return "RelWithDebInfo/"; 206 | } 207 | else 208 | { 209 | return "Release/"; 210 | } 211 | } 212 | public override string LibrariesPath { get { return "win64/"; } } 213 | public override List Architectures() { return new List { "" }; } 214 | public override string LibraryPrefixName { get { return ""; } } 215 | public override string LibraryPostfixName { get { return ".lib"; } } 216 | } 217 | 218 | public class TurboLinkPlatform_Android : TurboLinkPlatform 219 | { 220 | public override string LibrariesPath { get { return "android/"; } } 221 | public override List Architectures() { return new List { 222 | #if UE_5_0_OR_LATER 223 | "arm64-v8a/", "x86_64/" 224 | #else 225 | "armeabi-v7a/", "arm64-v8a/", "x86_64/" 226 | #endif 227 | }; } 228 | public override string LibraryPrefixName { get { return "lib"; } } 229 | public override string LibraryPostfixName { get { return ".a"; } } 230 | } 231 | 232 | public class TurboLinkPlatform_Linux : TurboLinkPlatform 233 | { 234 | public override string LibrariesPath { get { return "linux/"; } } 235 | public override List Architectures() { return new List { "" }; } 236 | public override string LibraryPrefixName { get { return "lib"; } } 237 | public override string LibraryPostfixName { get { return ".a"; } } 238 | } 239 | 240 | public class TurboLinkPlatform_PS5 : TurboLinkPlatform 241 | { 242 | public override string LibrariesPath { get { return "ps5/"; } } 243 | public override List Architectures() { return new List { "" }; } 244 | public override string LibraryPrefixName { get { return "lib"; } } 245 | public override string LibraryPostfixName { get { return ".a"; } } 246 | } 247 | 248 | public class TurboLinkPlatform_Mac : TurboLinkPlatform 249 | { 250 | public override string ConfigurationDir(UnrealTargetConfiguration Configuration) 251 | { 252 | return ""; 253 | } 254 | public override string LibrariesPath { get { return "mac/"; } } 255 | public override List Architectures() { return new List { "" }; } 256 | public override string LibraryPrefixName { get { return "lib"; } } 257 | public override string LibraryPostfixName { get { return ".a"; } } 258 | } 259 | 260 | public class TurboLinkPlatform_IOS : TurboLinkPlatform 261 | { 262 | public override string ConfigurationDir(UnrealTargetConfiguration Configuration) 263 | { 264 | return ""; 265 | } 266 | public override string LibrariesPath { get { return "ios/"; } } 267 | public override List Architectures() { return new List { "" }; } 268 | public override string LibraryPrefixName { get { return "lib"; } } 269 | public override string LibraryPostfixName { get { return ".a"; } } 270 | } 271 | --------------------------------------------------------------------------------