CUBlueprintLibrary.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. // Copyright 2018-current Getnamo. All Rights Reserved
  2. #include "CUBlueprintLibrary.h"
  3. #include "IImageWrapper.h"
  4. #include "IImageWrapperModule.h"
  5. #include "Runtime/Core/Public/Modules/ModuleManager.h"
  6. #include "Runtime/Core/Public/Async/Async.h"
  7. #include "Engine/Texture2D.h"
  8. #include "Runtime/Core/Public/HAL/ThreadSafeBool.h"
  9. #include "Runtime/RHI/Public/RHI.h"
  10. #include "Runtime/Core/Public/Misc/FileHelper.h"
  11. #include "Runtime/Engine/Public/OpusAudioInfo.h"
  12. #include "Runtime/Launch/Resources/Version.h"
  13. #include "Developer/TargetPlatform/Public/Interfaces/IAudioFormat.h"
  14. #include "CoreMinimal.h"
  15. #include "Engine/Engine.h"
  16. #include "CULambdaRunnable.h"
  17. #include "CUOpusCoder.h"
  18. #include "CUMeasureTimer.h"
  19. #include "Hash/CityHash.h"
  20. #pragma warning( push )
  21. #pragma warning( disable : 5046)
  22. //Render thread wrapper struct
  23. struct FUpdateTextureData
  24. {
  25. UTexture2D* Texture2D;
  26. FUpdateTextureRegion2D Region;
  27. uint32 Pitch;
  28. TArray64<uint8> BufferArray;
  29. TSharedPtr<IImageWrapper> Wrapper; //to keep the uncompressed data alive
  30. };
  31. FString UCUBlueprintLibrary::Conv_BytesToString(const TArray<uint8>& InArray)
  32. {
  33. FString ResultString;
  34. FFileHelper::BufferToString(ResultString, InArray.GetData(), InArray.Num());
  35. return ResultString;
  36. }
  37. TArray<uint8> UCUBlueprintLibrary::Conv_StringToBytes(FString InString)
  38. {
  39. TArray<uint8> ResultBytes;
  40. ResultBytes.Append((uint8*)TCHAR_TO_UTF8(*InString), InString.Len());
  41. return ResultBytes;
  42. }
  43. UTexture2D* UCUBlueprintLibrary::Conv_BytesToTexture(const TArray<uint8>& InBytes)
  44. {
  45. //Convert the UTexture2D back to an image
  46. UTexture2D* Texture = nullptr;
  47. IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked<IImageWrapperModule>(FName("ImageWrapper"));
  48. EImageFormat DetectedFormat = ImageWrapperModule.DetectImageFormat(InBytes.GetData(), InBytes.Num());
  49. TSharedPtr<IImageWrapper> ImageWrapper = ImageWrapperModule.CreateImageWrapper(DetectedFormat);
  50. //Set the compressed bytes - we need this information on game thread to be able to determine texture size, otherwise we'll need a complete async callback
  51. if (ImageWrapper.IsValid() && ImageWrapper->SetCompressed(InBytes.GetData(), InBytes.Num()))
  52. {
  53. //Create image given sizes
  54. Texture = UTexture2D::CreateTransient(ImageWrapper->GetWidth(), ImageWrapper->GetHeight(), PF_B8G8R8A8);
  55. Texture->UpdateResource();
  56. //Uncompress on a background thread pool
  57. FCULambdaRunnable::RunLambdaOnBackGroundThreadPool([ImageWrapper, Texture] {
  58. TArray64<uint8> UncompressedBGRA;
  59. if (ImageWrapper->GetRaw(ERGBFormat::BGRA, 8, UncompressedBGRA))
  60. {
  61. FUpdateTextureData* UpdateData = new FUpdateTextureData;
  62. UpdateData->Texture2D = Texture;
  63. UpdateData->Region = FUpdateTextureRegion2D(0, 0, 0, 0, Texture->GetSizeX(), Texture->GetSizeY());
  64. UpdateData->BufferArray = UncompressedBGRA;
  65. UpdateData->Pitch = Texture->GetSizeX() * 4;
  66. UpdateData->Wrapper = ImageWrapper;
  67. //enqueue texture copy
  68. ENQUEUE_RENDER_COMMAND(BytesToTextureCommand)(
  69. [UpdateData](FRHICommandList& CommandList)
  70. {
  71. RHIUpdateTexture2D(
  72. ((FTextureResource*)UpdateData->Texture2D->GetResource())->TextureRHI->GetTexture2D(),
  73. 0,
  74. UpdateData->Region,
  75. UpdateData->Pitch,
  76. UpdateData->BufferArray.GetData()
  77. );
  78. delete UpdateData; //now that we've updated the texture data, we can finally release any data we're holding on to
  79. });//End Enqueue
  80. }
  81. });
  82. }
  83. else
  84. {
  85. UE_LOG(LogTemp, Warning, TEXT("Invalid image format cannot decode %d"), (int32)DetectedFormat);
  86. }
  87. return Texture;
  88. }
  89. //one static coder, created based on need
  90. TSharedPtr<FCUOpusCoder> OpusCoder;
  91. TArray<uint8> UCUBlueprintLibrary::Conv_OpusBytesToWav(const TArray<uint8>& InBytes)
  92. {
  93. //FCUScopeTimer Timer(TEXT("Conv_OpusBytesToWav"));
  94. TArray<uint8> WavBytes;
  95. //Early exit condition
  96. if (InBytes.Num() == 0)
  97. {
  98. return WavBytes;
  99. }
  100. if (!OpusCoder)
  101. {
  102. OpusCoder = MakeShareable(new FCUOpusCoder());
  103. }
  104. TArray<uint8> PCMBytes;
  105. FCUOpusMinimalStream OpusStream;
  106. OpusCoder->DeserializeMinimal(InBytes, OpusStream);
  107. if (OpusCoder->DecodeStream(OpusStream, PCMBytes))
  108. {
  109. SerializeWaveFile(WavBytes, PCMBytes.GetData(), PCMBytes.Num(), OpusCoder->Channels, OpusCoder->SampleRate);
  110. }
  111. else
  112. {
  113. UE_LOG(LogTemp, Warning, TEXT("OpusMinimal to Wave Failed. DecodeStream returned false"));
  114. }
  115. return WavBytes;
  116. }
  117. TArray<uint8> UCUBlueprintLibrary::Conv_WavBytesToOpus(const TArray<uint8>& InBytes)
  118. {
  119. //FCUScopeTimer Timer(TEXT("Conv_WavBytesToOpus"));
  120. TArray<uint8> OpusBytes;
  121. FWaveModInfo WaveInfo;
  122. if (!WaveInfo.ReadWaveInfo(InBytes.GetData(), InBytes.Num()))
  123. {
  124. return OpusBytes;
  125. }
  126. if (!OpusCoder)
  127. {
  128. OpusCoder = MakeShareable(new FCUOpusCoder());
  129. }
  130. TArray<uint8> PCMBytes = TArray<uint8>(WaveInfo.SampleDataStart, WaveInfo.SampleDataSize);
  131. FCUOpusMinimalStream OpusStream;
  132. OpusCoder->EncodeStream(PCMBytes, OpusStream);
  133. TArray<uint8> SerializedBytes;
  134. OpusCoder->SerializeMinimal(OpusStream, SerializedBytes);
  135. return SerializedBytes;
  136. }
  137. USoundWave* UCUBlueprintLibrary::Conv_WavBytesToSoundWave(const TArray<uint8>& InBytes)
  138. {
  139. USoundWave* SoundWave;
  140. //Allocate based on thread
  141. if (IsInGameThread())
  142. {
  143. SoundWave = NewObject<USoundWaveProcedural>(USoundWaveProcedural::StaticClass());
  144. SetSoundWaveFromWavBytes((USoundWaveProcedural*)SoundWave, InBytes);
  145. }
  146. else
  147. {
  148. //We will go to another thread, copy our bytes
  149. TArray<uint8> CopiedBytes = InBytes;
  150. FThreadSafeBool bAllocationComplete = false;
  151. AsyncTask(ENamedThreads::GameThread, [&bAllocationComplete, &SoundWave]
  152. {
  153. SoundWave = NewObject<USoundWaveProcedural>(USoundWaveProcedural::StaticClass());
  154. bAllocationComplete = true;
  155. });
  156. //block while not complete
  157. while (!bAllocationComplete)
  158. {
  159. //100micros sleep, this should be very quick
  160. FPlatformProcess::Sleep(0.0001f);
  161. };
  162. SetSoundWaveFromWavBytes((USoundWaveProcedural*)SoundWave, CopiedBytes);
  163. }
  164. return SoundWave;
  165. }
  166. TArray<uint8> UCUBlueprintLibrary::Conv_SoundWaveToWavBytes(USoundWave* SoundWave)
  167. {
  168. TArray<uint8> PCMBytes;
  169. TArray<uint8> WavBytes;
  170. //memcpy raw data from soundwave, hmm this won't work for procedurals...
  171. const void* LockedData = SoundWave->GetResourceData();
  172. PCMBytes.SetNumUninitialized(SoundWave->GetResourceSize());
  173. FMemory::Memcpy(PCMBytes.GetData(), LockedData, PCMBytes.Num());
  174. //add wav header
  175. SerializeWaveFile(WavBytes, PCMBytes.GetData(), PCMBytes.Num(), SoundWave->NumChannels, SoundWave->GetSampleRateForCurrentPlatform());
  176. return WavBytes;
  177. }
  178. void UCUBlueprintLibrary::Conv_CompactBytesToTransforms(const TArray<uint8>& InCompactBytes, TArray<FTransform>& OutTransforms)
  179. {
  180. TArray<float> FloatView;
  181. FloatView.SetNumUninitialized(InCompactBytes.Num() / 4);
  182. FPlatformMemory::Memcpy(FloatView.GetData(), InCompactBytes.GetData(), InCompactBytes.Num());
  183. //is our float array exactly divisible by 9?
  184. if (FloatView.Num() % 9 != 0)
  185. {
  186. UE_LOG(LogTemp, Log, TEXT("Conv_CompactBytesToTransforms::float array is not divisible by 9"));
  187. return;
  188. }
  189. int32 TransformNum = FloatView.Num() / 9;
  190. OutTransforms.SetNumUninitialized(TransformNum);
  191. for (int i = 0; i < FloatView.Num() - 8; i += 9)
  192. {
  193. OutTransforms[i/9] = FTransform(FRotator(FloatView[i], FloatView[i+1], FloatView[i+2]), FVector(FloatView[i+3], FloatView[i + 4], FloatView[i + 5]), FVector(FloatView[i+6], FloatView[i+7], FloatView[i+8]));
  194. }
  195. }
  196. void UCUBlueprintLibrary::Conv_CompactPositionBytesToTransforms(const TArray<uint8>& InCompactBytes, TArray<FTransform>& OutTransforms)
  197. {
  198. TArray<float> FloatView;
  199. FloatView.SetNumUninitialized(InCompactBytes.Num() / 4);
  200. FPlatformMemory::Memcpy(FloatView.GetData(), InCompactBytes.GetData(), InCompactBytes.Num());
  201. //is our float array exactly divisible by 3?
  202. if (FloatView.Num() % 3 != 0)
  203. {
  204. UE_LOG(LogTemp, Log, TEXT("Conv_CompactPositionBytesToTransforms::float array is not divisible by 3"));
  205. return;
  206. }
  207. int32 TransformNum = FloatView.Num() / 3;
  208. OutTransforms.SetNumUninitialized(TransformNum);
  209. for (int i = 0; i < FloatView.Num() - 2; i += 3)
  210. {
  211. OutTransforms[i / 3] = FTransform(FVector(FloatView[i], FloatView[i + 1], FloatView[i + 2]));
  212. }
  213. }
  214. void UCUBlueprintLibrary::SetSoundWaveFromWavBytes(USoundWaveProcedural* InSoundWave, const TArray<uint8>& InBytes)
  215. {
  216. FWaveModInfo WaveInfo;
  217. FString ErrorReason;
  218. if (WaveInfo.ReadWaveInfo(InBytes.GetData(), InBytes.Num(), &ErrorReason))
  219. {
  220. //copy header info
  221. int32 DurationDiv = *WaveInfo.pChannels * *WaveInfo.pBitsPerSample * *WaveInfo.pSamplesPerSec;
  222. if (DurationDiv)
  223. {
  224. InSoundWave->Duration = *WaveInfo.pWaveDataSize * 8.0f / DurationDiv;
  225. }
  226. else
  227. {
  228. InSoundWave->Duration = 0.0f;
  229. }
  230. InSoundWave->SetSampleRate(*WaveInfo.pSamplesPerSec);
  231. InSoundWave->NumChannels = *WaveInfo.pChannels;
  232. //SoundWaveProc->RawPCMDataSize = WaveInfo.SampleDataSize;
  233. InSoundWave->bLooping = false;
  234. InSoundWave->SoundGroup = ESoundGroup::SOUNDGROUP_Default;
  235. //Queue actual audio data
  236. InSoundWave->QueueAudio(WaveInfo.SampleDataStart, WaveInfo.SampleDataSize);
  237. }
  238. else
  239. {
  240. UE_LOG(LogTemp, Log, TEXT("SetSoundWaveFromWavBytes::WaveRead error: %s"), *ErrorReason);
  241. }
  242. }
  243. TFuture<UTexture2D*> UCUBlueprintLibrary::Conv_BytesToTexture_Async(const TArray<uint8>& InBytes)
  244. {
  245. //Running this on a background thread
  246. return Async(EAsyncExecution::Thread, [InBytes]
  247. {
  248. //Create wrapper pointer we can share easily across threads
  249. struct FDataHolder
  250. {
  251. UTexture2D* Texture = nullptr;
  252. };
  253. TSharedPtr<FDataHolder> Holder = MakeShareable(new FDataHolder);
  254. FThreadSafeBool bLoadModuleComplete = false;
  255. IImageWrapperModule* ImageWrapperModule;
  256. AsyncTask(ENamedThreads::GameThread, [&bLoadModuleComplete, Holder, &ImageWrapperModule]
  257. {
  258. ImageWrapperModule = &FModuleManager::LoadModuleChecked<IImageWrapperModule>(FName("ImageWrapper"));
  259. bLoadModuleComplete = true;
  260. });
  261. while (!bLoadModuleComplete)
  262. {
  263. FPlatformProcess::Sleep(0.001f);
  264. }
  265. EImageFormat DetectedFormat = ImageWrapperModule->DetectImageFormat(InBytes.GetData(), InBytes.Num());
  266. TSharedPtr<IImageWrapper> ImageWrapper = ImageWrapperModule->CreateImageWrapper(DetectedFormat);
  267. if (!(ImageWrapper.IsValid() && ImageWrapper->SetCompressed(InBytes.GetData(), InBytes.Num())))
  268. {
  269. UE_LOG(LogTemp, Warning, TEXT("Invalid image format cannot decode %d"), (int32)DetectedFormat);
  270. return (UTexture2D*)nullptr;
  271. }
  272. //Create image given sizes
  273. //Creation of UTexture needs to happen on game thread
  274. FThreadSafeBool bAllocationComplete = false;
  275. FIntPoint Size;
  276. Size.X = ImageWrapper->GetWidth();
  277. Size.Y = ImageWrapper->GetHeight();
  278. AsyncTask(ENamedThreads::GameThread, [&bAllocationComplete, Holder, Size]
  279. {
  280. Holder->Texture = UTexture2D::CreateTransient(Size.X, Size.Y, PF_B8G8R8A8);
  281. Holder->Texture->UpdateResource();
  282. bAllocationComplete = true;
  283. });
  284. while (!bAllocationComplete)
  285. {
  286. //sleep 10ms intervals
  287. FPlatformProcess::Sleep(0.001f);
  288. }
  289. //Uncompress on a background thread pool
  290. TArray64<uint8> UncompressedBGRA;
  291. if (!ImageWrapper->GetRaw(ERGBFormat::BGRA, 8, UncompressedBGRA))
  292. {
  293. return (UTexture2D*)nullptr;
  294. }
  295. FUpdateTextureData* UpdateData = new FUpdateTextureData;
  296. UpdateData->Texture2D = Holder->Texture;
  297. UpdateData->Region = FUpdateTextureRegion2D(0, 0, 0, 0, Size.X, Size.Y);
  298. UpdateData->BufferArray = UncompressedBGRA;
  299. UpdateData->Pitch = Size.X * 4;
  300. UpdateData->Wrapper = ImageWrapper;
  301. //This command sends it to the render thread
  302. ENQUEUE_RENDER_COMMAND(BytesToTextureAsyncCommand)(
  303. [UpdateData](FRHICommandList& CommandList)
  304. {
  305. RHIUpdateTexture2D(
  306. ((FTextureResource*)UpdateData->Texture2D->GetResource())->TextureRHI->GetTexture2D(),
  307. 0,
  308. UpdateData->Region,
  309. UpdateData->Pitch,
  310. UpdateData->BufferArray.GetData()
  311. );
  312. delete UpdateData; //now that we've updated the texture data, we can finally release any data we're holding on to
  313. });//End Enqueue
  314. return Holder->Texture;
  315. });//End async
  316. }
  317. bool UCUBlueprintLibrary::Conv_TextureToBytes(UTexture2D* Texture, TArray<uint8>& OutBuffer, EImageFormatBPType Format /*= EImageFormatBPType::JPEG*/)
  318. {
  319. if (!Texture || !Texture->IsValidLowLevel())
  320. {
  321. return false;
  322. }
  323. //Get our wrapper module
  324. IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked<IImageWrapperModule>(FName("ImageWrapper"));
  325. TSharedPtr<IImageWrapper> ImageWrapper = ImageWrapperModule.CreateImageWrapper((EImageFormat)Format);
  326. int32 Width = Texture->GetPlatformData()->Mips[0].SizeX;
  327. int32 Height = Texture->GetPlatformData()->Mips[0].SizeY;
  328. int32 DataLength = Width * Height * 4;
  329. void* TextureDataPointer = Texture->GetPlatformData()->Mips[0].BulkData.Lock(LOCK_READ_ONLY);
  330. ImageWrapper->SetRaw(TextureDataPointer, DataLength, Width, Height, ERGBFormat::BGRA, 8);
  331. //This part can take a while, has performance implications
  332. OutBuffer = ImageWrapper->GetCompressed();
  333. Texture->GetPlatformData()->Mips[0].BulkData.Unlock();
  334. return true;
  335. }
  336. FString UCUBlueprintLibrary::NowUTCString()
  337. {
  338. return FDateTime::UtcNow().ToString();
  339. }
  340. FString UCUBlueprintLibrary::GetLoginId()
  341. {
  342. return FPlatformMisc::GetLoginId();
  343. }
  344. int32 UCUBlueprintLibrary::ToHashCode(const FString& String)
  345. {
  346. return (int32)CityHash32(TCHAR_TO_ANSI(*String), String.Len());
  347. }
  348. void UCUBlueprintLibrary::MeasureTimerStart(const FString& Category /*= TEXT("TimeTaken")*/)
  349. {
  350. FCUMeasureTimer::Tick(Category);
  351. }
  352. float UCUBlueprintLibrary::MeasureTimerStop(const FString& Category /*= TEXT("TimeTaken")*/, bool bShouldLogResult /*= true*/)
  353. {
  354. return (float)FCUMeasureTimer::Tock(Category, bShouldLogResult);
  355. }
  356. void UCUBlueprintLibrary::CallFunctionOnThread(const FString& FunctionName, ESIOCallbackType ThreadType, UObject* WorldContextObject /*= nullptr*/)
  357. {
  358. UObject* Target = WorldContextObject;
  359. if (!Target->IsValidLowLevel())
  360. {
  361. UE_LOG(LogTemp, Warning, TEXT("CallFunctionOnThread: Target not found for '%s'"), *FunctionName);
  362. return;
  363. }
  364. UFunction* Function = Target->FindFunction(FName(*FunctionName));
  365. if (nullptr == Function)
  366. {
  367. UE_LOG(LogTemp, Warning, TEXT("CallFunctionOnThread: Function not found '%s'"), *FunctionName);
  368. return;
  369. }
  370. switch (ThreadType)
  371. {
  372. case CALLBACK_GAME_THREAD:
  373. if (IsInGameThread())
  374. {
  375. Target->ProcessEvent(Function, nullptr);
  376. }
  377. else
  378. {
  379. FCULambdaRunnable::RunShortLambdaOnGameThread([Function, Target]
  380. {
  381. if (Target->IsValidLowLevel())
  382. {
  383. Target->ProcessEvent(Function, nullptr);
  384. }
  385. });
  386. }
  387. break;
  388. case CALLBACK_BACKGROUND_THREADPOOL:
  389. FCULambdaRunnable::RunLambdaOnBackGroundThreadPool([Function, Target]
  390. {
  391. if (Target->IsValidLowLevel())
  392. {
  393. Target->ProcessEvent(Function, nullptr);
  394. }
  395. });
  396. break;
  397. case CALLBACK_BACKGROUND_TASKGRAPH:
  398. FCULambdaRunnable::RunShortLambdaOnBackGroundTask([Function, Target]
  399. {
  400. if (Target->IsValidLowLevel())
  401. {
  402. Target->ProcessEvent(Function, nullptr);
  403. }
  404. });
  405. break;
  406. default:
  407. break;
  408. }
  409. }
  410. void UCUBlueprintLibrary::CallFunctionOnThreadGraphReturn(const FString& FunctionName, ESIOCallbackType ThreadType, struct FLatentActionInfo LatentInfo, UObject* WorldContextObject /*= nullptr*/)
  411. {
  412. UObject* Target = WorldContextObject;
  413. FCULatentAction* LatentAction = FCULatentAction::CreateLatentAction(LatentInfo, Target);
  414. if (!Target->IsValidLowLevel())
  415. {
  416. UE_LOG(LogTemp, Warning, TEXT("CallFunctionOnThread: Target not found for '%s'"), *FunctionName);
  417. LatentAction->Call();
  418. return;
  419. }
  420. UFunction* Function = Target->FindFunction(FName(*FunctionName));
  421. if (nullptr == Function)
  422. {
  423. UE_LOG(LogTemp, Warning, TEXT("CallFunctionOnThread: Function not found '%s'"), *FunctionName);
  424. LatentAction->Call();
  425. return;
  426. }
  427. switch (ThreadType)
  428. {
  429. case CALLBACK_GAME_THREAD:
  430. if (IsInGameThread())
  431. {
  432. Target->ProcessEvent(Function, nullptr);
  433. LatentAction->Call();
  434. }
  435. else
  436. {
  437. FCULambdaRunnable::RunShortLambdaOnGameThread([Function, Target, LatentAction]
  438. {
  439. if (Target->IsValidLowLevel())
  440. {
  441. Target->ProcessEvent(Function, nullptr);
  442. LatentAction->Call();
  443. }
  444. });
  445. }
  446. break;
  447. case CALLBACK_BACKGROUND_THREADPOOL:
  448. FCULambdaRunnable::RunLambdaOnBackGroundThreadPool([Function, Target, LatentAction]
  449. {
  450. if (Target->IsValidLowLevel())
  451. {
  452. Target->ProcessEvent(Function, nullptr);
  453. LatentAction->Call();
  454. }
  455. });
  456. break;
  457. case CALLBACK_BACKGROUND_TASKGRAPH:
  458. FCULambdaRunnable::RunShortLambdaOnBackGroundTask([Function, Target, LatentAction]
  459. {
  460. if (Target->IsValidLowLevel())
  461. {
  462. Target->ProcessEvent(Function, nullptr);
  463. LatentAction->Call();
  464. }
  465. });
  466. break;
  467. default:
  468. break;
  469. }
  470. }
  471. #pragma warning( pop )