feat: add ue_scene websocket integration
This commit is contained in:
22
unreal/Source/Planet/Planet.Build.cs
Normal file
22
unreal/Source/Planet/Planet.Build.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class Planet : ModuleRules
|
||||
{
|
||||
public Planet(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicDependencyModuleNames.AddRange(
|
||||
new[]
|
||||
{
|
||||
"Core",
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"HTTP",
|
||||
"Json",
|
||||
"JsonUtilities",
|
||||
"WebSockets",
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
677
unreal/Source/Planet/Private/UeSceneWebSocketClient.cpp
Normal file
677
unreal/Source/Planet/Private/UeSceneWebSocketClient.cpp
Normal file
@@ -0,0 +1,677 @@
|
||||
#include "UeSceneWebSocketClient.h"
|
||||
|
||||
#include "Dom/JsonObject.h"
|
||||
#include "Dom/JsonValue.h"
|
||||
#include "HttpModule.h"
|
||||
#include "JsonObjectConverter.h"
|
||||
#include "Serialization/JsonReader.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
#include "WebSocketsModule.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr double GeoPointMergeTolerance = 1e-4;
|
||||
|
||||
bool TryParseGeoPoint(const TSharedPtr<FJsonObject>& PointObject, FVector& OutGeoPoint)
|
||||
{
|
||||
if (!PointObject.IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
double Latitude = 0.0;
|
||||
double Longitude = 0.0;
|
||||
double Altitude = 0.0;
|
||||
if (!PointObject->TryGetNumberField(TEXT("lat"), Latitude)
|
||||
|| !PointObject->TryGetNumberField(TEXT("lng"), Longitude))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
PointObject->TryGetNumberField(TEXT("alt"), Altitude);
|
||||
OutGeoPoint = FVector(Longitude, Latitude, Altitude);
|
||||
return true;
|
||||
}
|
||||
|
||||
FString ResolveCableName(const TSharedPtr<FJsonObject>& ItemObject)
|
||||
{
|
||||
FString CableName;
|
||||
|
||||
const TSharedPtr<FJsonObject>* LabelsObject = nullptr;
|
||||
if (ItemObject->TryGetObjectField(TEXT("labels"), LabelsObject)
|
||||
&& LabelsObject != nullptr
|
||||
&& LabelsObject->IsValid())
|
||||
{
|
||||
(*LabelsObject)->TryGetStringField(TEXT("title"), CableName);
|
||||
}
|
||||
|
||||
if (CableName.IsEmpty())
|
||||
{
|
||||
ItemObject->TryGetStringField(TEXT("id"), CableName);
|
||||
}
|
||||
|
||||
return CableName;
|
||||
}
|
||||
|
||||
bool AreGeoPointsConnected(const FVector& A, const FVector& B)
|
||||
{
|
||||
return FMath::Abs(A.X - B.X) <= GeoPointMergeTolerance
|
||||
&& FMath::Abs(A.Y - B.Y) <= GeoPointMergeTolerance
|
||||
&& FMath::Abs(A.Z - B.Z) <= GeoPointMergeTolerance;
|
||||
}
|
||||
|
||||
void AppendMergedSegment(
|
||||
TArray<TArray<FVector>>& MergedSegments,
|
||||
const TArray<FVector>& IncomingSegment
|
||||
)
|
||||
{
|
||||
if (IncomingSegment.Num() < 2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (MergedSegments.Num() == 0)
|
||||
{
|
||||
MergedSegments.Add(IncomingSegment);
|
||||
return;
|
||||
}
|
||||
|
||||
TArray<FVector>& LastMergedSegment = MergedSegments.Last();
|
||||
if (LastMergedSegment.Num() == 0)
|
||||
{
|
||||
LastMergedSegment = IncomingSegment;
|
||||
return;
|
||||
}
|
||||
|
||||
if (AreGeoPointsConnected(LastMergedSegment.Last(), IncomingSegment[0]))
|
||||
{
|
||||
for (int32 Index = 1; Index < IncomingSegment.Num(); ++Index)
|
||||
{
|
||||
LastMergedSegment.Add(IncomingSegment[Index]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
MergedSegments.Add(IncomingSegment);
|
||||
}
|
||||
}
|
||||
|
||||
UUeSceneWebSocketClient::UUeSceneWebSocketClient()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
}
|
||||
|
||||
void UUeSceneWebSocketClient::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
}
|
||||
|
||||
void UUeSceneWebSocketClient::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||
{
|
||||
DisconnectFromBackend();
|
||||
Super::EndPlay(EndPlayReason);
|
||||
}
|
||||
|
||||
void UUeSceneWebSocketClient::ConnectToBackend()
|
||||
{
|
||||
if (Username.IsEmpty() || Password.IsEmpty())
|
||||
{
|
||||
EmitError(TEXT("Username / Password 不能为空。"));
|
||||
return;
|
||||
}
|
||||
|
||||
RequestAccessToken();
|
||||
}
|
||||
|
||||
void UUeSceneWebSocketClient::DisconnectFromBackend()
|
||||
{
|
||||
if (Socket.IsValid())
|
||||
{
|
||||
Socket->Close();
|
||||
Socket.Reset();
|
||||
}
|
||||
|
||||
PendingFullScene.Reset();
|
||||
OnConnectedChanged.Broadcast(false);
|
||||
}
|
||||
|
||||
void UUeSceneWebSocketClient::SendSubscribe()
|
||||
{
|
||||
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
|
||||
Root->SetStringField(TEXT("type"), TEXT("subscribe"));
|
||||
|
||||
TSharedRef<FJsonObject> Data = MakeShared<FJsonObject>();
|
||||
TArray<TSharedPtr<FJsonValue>> Channels;
|
||||
for (const FString& Channel : DefaultChannels)
|
||||
{
|
||||
Channels.Add(MakeShared<FJsonValueString>(Channel));
|
||||
}
|
||||
Data->SetArrayField(TEXT("channels"), Channels);
|
||||
Root->SetObjectField(TEXT("data"), Data);
|
||||
|
||||
FString Payload;
|
||||
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Payload);
|
||||
FJsonSerializer::Serialize(Root, Writer);
|
||||
SendJsonString(Payload);
|
||||
}
|
||||
|
||||
void UUeSceneWebSocketClient::SendInitialSyncRequest()
|
||||
{
|
||||
SendSyncRequest(TEXT("initial_connect"), INDEX_NONE);
|
||||
}
|
||||
|
||||
void UUeSceneWebSocketClient::SendSyncRequest(const FString& Reason, int32 InLastSequence)
|
||||
{
|
||||
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
|
||||
Root->SetStringField(TEXT("type"), TEXT("sync_request"));
|
||||
|
||||
TSharedRef<FJsonObject> Data = MakeShared<FJsonObject>();
|
||||
Data->SetStringField(TEXT("channel"), TEXT("ue_scene"));
|
||||
Data->SetStringField(TEXT("reason"), Reason);
|
||||
if (InLastSequence >= 0)
|
||||
{
|
||||
Data->SetNumberField(TEXT("last_sequence"), InLastSequence);
|
||||
}
|
||||
Root->SetObjectField(TEXT("data"), Data);
|
||||
|
||||
FString Payload;
|
||||
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Payload);
|
||||
FJsonSerializer::Serialize(Root, Writer);
|
||||
SendJsonString(Payload);
|
||||
}
|
||||
|
||||
void UUeSceneWebSocketClient::RequestAccessToken()
|
||||
{
|
||||
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = FHttpModule::Get().CreateRequest();
|
||||
Request->SetURL(BuildHttpUrl(TEXT("/api/v1/auth/login")));
|
||||
Request->SetVerb(TEXT("POST"));
|
||||
Request->SetHeader(TEXT("Content-Type"), TEXT("application/x-www-form-urlencoded"));
|
||||
Request->SetContentAsString(
|
||||
FString::Printf(TEXT("username=%s&password=%s"), *Username, *Password)
|
||||
);
|
||||
Request->OnProcessRequestComplete().BindUObject(
|
||||
this, &UUeSceneWebSocketClient::HandleLoginResponse
|
||||
);
|
||||
Request->ProcessRequest();
|
||||
}
|
||||
|
||||
void UUeSceneWebSocketClient::HandleLoginResponse(
|
||||
FHttpRequestPtr Request,
|
||||
FHttpResponsePtr Response,
|
||||
bool bSucceeded
|
||||
)
|
||||
{
|
||||
if (!bSucceeded || !Response.IsValid())
|
||||
{
|
||||
EmitError(TEXT("登录请求失败。"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (Response->GetResponseCode() < 200 || Response->GetResponseCode() >= 300)
|
||||
{
|
||||
EmitError(FString::Printf(TEXT("登录失败,HTTP %d"), Response->GetResponseCode()));
|
||||
return;
|
||||
}
|
||||
|
||||
TSharedPtr<FJsonObject> Root;
|
||||
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString());
|
||||
if (!FJsonSerializer::Deserialize(Reader, Root) || !Root.IsValid())
|
||||
{
|
||||
EmitError(TEXT("登录响应 JSON 解析失败。"));
|
||||
return;
|
||||
}
|
||||
|
||||
AccessToken = Root->GetStringField(TEXT("access_token"));
|
||||
if (AccessToken.IsEmpty())
|
||||
{
|
||||
EmitError(TEXT("登录响应里没有 access_token。"));
|
||||
return;
|
||||
}
|
||||
|
||||
OpenWebSocket();
|
||||
}
|
||||
|
||||
void UUeSceneWebSocketClient::OpenWebSocket()
|
||||
{
|
||||
if (!FModuleManager::Get().IsModuleLoaded(TEXT("WebSockets")))
|
||||
{
|
||||
FModuleManager::Get().LoadModule(TEXT("WebSockets"));
|
||||
}
|
||||
|
||||
Socket = FWebSocketsModule::Get().CreateWebSocket(BuildWsUrl());
|
||||
BindSocketEvents();
|
||||
Socket->Connect();
|
||||
}
|
||||
|
||||
void UUeSceneWebSocketClient::BindSocketEvents()
|
||||
{
|
||||
if (!Socket.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Socket->OnConnected().AddLambda([this]()
|
||||
{
|
||||
OnConnectedChanged.Broadcast(true);
|
||||
});
|
||||
|
||||
Socket->OnConnectionError().AddLambda([this](const FString& Error)
|
||||
{
|
||||
EmitError(FString::Printf(TEXT("WebSocket 连接失败: %s"), *Error));
|
||||
});
|
||||
|
||||
Socket->OnClosed().AddLambda([this](int32 StatusCode, const FString& Reason, bool bWasClean)
|
||||
{
|
||||
OnConnectedChanged.Broadcast(false);
|
||||
});
|
||||
|
||||
Socket->OnMessage().AddLambda([this](const FString& Message)
|
||||
{
|
||||
OnRawMessage.Broadcast(Message);
|
||||
TryBroadcastSceneFrame(Message);
|
||||
});
|
||||
}
|
||||
|
||||
void UUeSceneWebSocketClient::EmitError(const FString& ErrorMessage)
|
||||
{
|
||||
OnError.Broadcast(ErrorMessage);
|
||||
}
|
||||
|
||||
void UUeSceneWebSocketClient::TryBroadcastSceneFrame(const FString& Message)
|
||||
{
|
||||
TSharedPtr<FJsonObject> Root;
|
||||
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Message);
|
||||
if (!FJsonSerializer::Deserialize(Reader, Root) || !Root.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FString Type;
|
||||
if (!Root->TryGetStringField(TEXT("type"), Type) || Type != TEXT("data_frame"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FString Channel;
|
||||
if (!Root->TryGetStringField(TEXT("channel"), Channel) || Channel != TEXT("ue_scene"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const TSharedPtr<FJsonObject>* DataObject = nullptr;
|
||||
if (!Root->TryGetObjectField(TEXT("data"), DataObject) || DataObject == nullptr || !DataObject->IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
TSharedPtr<FJsonObject> EffectiveDataObject = *DataObject;
|
||||
|
||||
FString UpdateType = TEXT("unknown");
|
||||
EffectiveDataObject->TryGetStringField(TEXT("update_type"), UpdateType);
|
||||
|
||||
int32 Sequence = 0;
|
||||
EffectiveDataObject->TryGetNumberField(TEXT("sequence"), Sequence);
|
||||
|
||||
int32 TotalRecords = 0;
|
||||
const TSharedPtr<FJsonObject>* PayloadObject = nullptr;
|
||||
if (EffectiveDataObject->TryGetObjectField(TEXT("payload"), PayloadObject) && PayloadObject != nullptr && PayloadObject->IsValid())
|
||||
{
|
||||
const TSharedPtr<FJsonObject>* MetaObject = nullptr;
|
||||
if ((*PayloadObject)->TryGetObjectField(TEXT("meta"), MetaObject) && MetaObject != nullptr && MetaObject->IsValid())
|
||||
{
|
||||
(*MetaObject)->TryGetNumberField(TEXT("total_records"), TotalRecords);
|
||||
}
|
||||
}
|
||||
|
||||
if (UpdateType == TEXT("full"))
|
||||
{
|
||||
TSharedPtr<FJsonObject> AssembledDataObject;
|
||||
if (!TryAssembleFullScene(EffectiveDataObject, Sequence, TotalRecords, AssembledDataObject))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (AssembledDataObject.IsValid())
|
||||
{
|
||||
EffectiveDataObject = AssembledDataObject;
|
||||
}
|
||||
}
|
||||
|
||||
LastSequence = Sequence;
|
||||
OnSceneFrame.Broadcast(UpdateType, Sequence, TotalRecords);
|
||||
BroadcastScenePayload(UpdateType, Sequence, EffectiveDataObject);
|
||||
}
|
||||
|
||||
bool UUeSceneWebSocketClient::TryAssembleFullScene(
|
||||
const TSharedPtr<FJsonObject>& DataObject,
|
||||
int32 Sequence,
|
||||
int32 TotalRecords,
|
||||
TSharedPtr<FJsonObject>& OutAssembledDataObject
|
||||
)
|
||||
{
|
||||
OutAssembledDataObject = nullptr;
|
||||
|
||||
const TSharedPtr<FJsonObject>* PayloadObject = nullptr;
|
||||
if (!DataObject->TryGetObjectField(TEXT("payload"), PayloadObject) || PayloadObject == nullptr || !PayloadObject->IsValid())
|
||||
{
|
||||
EmitError(TEXT("Full scene payload is missing."));
|
||||
return false;
|
||||
}
|
||||
|
||||
const TSharedPtr<FJsonObject>* MetaObject = nullptr;
|
||||
if (!(*PayloadObject)->TryGetObjectField(TEXT("meta"), MetaObject) || MetaObject == nullptr || !MetaObject->IsValid())
|
||||
{
|
||||
EmitError(TEXT("Full scene meta is missing."));
|
||||
return false;
|
||||
}
|
||||
|
||||
bool bChunked = false;
|
||||
(*MetaObject)->TryGetBoolField(TEXT("chunked"), bChunked);
|
||||
if (!bChunked)
|
||||
{
|
||||
OutAssembledDataObject = DataObject;
|
||||
PendingFullScene.Reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
int32 TransportChunkIndex = 0;
|
||||
int32 TransportChunkCount = 0;
|
||||
int32 LayerChunkIndex = 0;
|
||||
int32 LayerChunkCount = 0;
|
||||
FString LayerName;
|
||||
(*MetaObject)->TryGetNumberField(TEXT("transport_chunk_index"), TransportChunkIndex);
|
||||
(*MetaObject)->TryGetNumberField(TEXT("transport_chunk_count"), TransportChunkCount);
|
||||
(*MetaObject)->TryGetNumberField(TEXT("layer_chunk_index"), LayerChunkIndex);
|
||||
(*MetaObject)->TryGetNumberField(TEXT("layer_chunk_count"), LayerChunkCount);
|
||||
(*MetaObject)->TryGetStringField(TEXT("layer_name"), LayerName);
|
||||
|
||||
if (TransportChunkIndex <= 0 || TransportChunkCount <= 0 || LayerChunkIndex <= 0 || LayerChunkCount <= 0 || LayerName.IsEmpty())
|
||||
{
|
||||
EmitError(TEXT("Chunked full scene metadata is incomplete."));
|
||||
PendingFullScene.Reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (PendingFullScene.Sequence != Sequence)
|
||||
{
|
||||
PendingFullScene.Reset();
|
||||
PendingFullScene.Sequence = Sequence;
|
||||
PendingFullScene.ExpectedTransportChunkCount = TransportChunkCount;
|
||||
PendingFullScene.TotalRecords = TotalRecords;
|
||||
PendingFullScene.DisplayProfile = DataObject->GetObjectField(TEXT("display_profile"));
|
||||
PendingFullScene.CameraState = DataObject->GetObjectField(TEXT("camera_state"));
|
||||
PendingFullScene.Meta = MakeShared<FJsonObject>(*MetaObject->Get());
|
||||
DataObject->TryGetStringField(TEXT("cluster_time"), PendingFullScene.ClusterTime);
|
||||
}
|
||||
|
||||
if (PendingFullScene.Sequence != Sequence || PendingFullScene.ExpectedTransportChunkCount != TransportChunkCount)
|
||||
{
|
||||
EmitError(TEXT("Chunked full scene sequence mismatch."));
|
||||
PendingFullScene.Reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (PendingFullScene.ReceivedTransportChunkIndices.Contains(TransportChunkIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
PendingFullScene.ReceivedTransportChunkIndices.Add(TransportChunkIndex);
|
||||
|
||||
const TSharedPtr<FJsonObject>* LayersObject = nullptr;
|
||||
if (!(*PayloadObject)->TryGetObjectField(TEXT("layers"), LayersObject) || LayersObject == nullptr || !LayersObject->IsValid())
|
||||
{
|
||||
EmitError(TEXT("Chunked full scene layers are missing."));
|
||||
PendingFullScene.Reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
const TSharedPtr<FJsonObject>* LayerDataObject = nullptr;
|
||||
if (!(*LayersObject)->TryGetObjectField(LayerName, LayerDataObject) || LayerDataObject == nullptr || !LayerDataObject->IsValid())
|
||||
{
|
||||
EmitError(TEXT("Chunked full scene layer payload is missing."));
|
||||
PendingFullScene.Reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
FLayerChunkAccumulator& LayerAccumulator = PendingFullScene.Layers.FindOrAdd(LayerName);
|
||||
if (LayerAccumulator.ExpectedChunkCount == 0)
|
||||
{
|
||||
LayerAccumulator.ExpectedChunkCount = LayerChunkCount;
|
||||
LayerDataObject->Get()->TryGetNumberField(TEXT("revision"), LayerAccumulator.Revision);
|
||||
}
|
||||
else if (LayerAccumulator.ExpectedChunkCount != LayerChunkCount)
|
||||
{
|
||||
EmitError(TEXT("Layer chunk count changed while assembling full scene."));
|
||||
PendingFullScene.Reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (LayerAccumulator.ReceivedChunkIndices.Contains(LayerChunkIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const TArray<TSharedPtr<FJsonValue>>* LayerItems = nullptr;
|
||||
if (!LayerDataObject->Get()->TryGetArrayField(TEXT("items"), LayerItems) || LayerItems == nullptr)
|
||||
{
|
||||
EmitError(TEXT("Chunked full scene items are missing."));
|
||||
PendingFullScene.Reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
LayerAccumulator.ReceivedChunkIndices.Add(LayerChunkIndex);
|
||||
LayerAccumulator.Items.Append(*LayerItems);
|
||||
|
||||
if (PendingFullScene.ReceivedTransportChunkIndices.Num() != PendingFullScene.ExpectedTransportChunkCount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const TPair<FString, FLayerChunkAccumulator>& LayerPair : PendingFullScene.Layers)
|
||||
{
|
||||
if (LayerPair.Value.ReceivedChunkIndices.Num() != LayerPair.Value.ExpectedChunkCount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> AssembledDataObject = MakeShared<FJsonObject>();
|
||||
AssembledDataObject->SetStringField(TEXT("update_type"), TEXT("full"));
|
||||
AssembledDataObject->SetNumberField(TEXT("sequence"), Sequence);
|
||||
AssembledDataObject->SetStringField(TEXT("cluster_time"), PendingFullScene.ClusterTime);
|
||||
if (PendingFullScene.DisplayProfile.IsValid())
|
||||
{
|
||||
AssembledDataObject->SetObjectField(TEXT("display_profile"), PendingFullScene.DisplayProfile);
|
||||
}
|
||||
if (PendingFullScene.CameraState.IsValid())
|
||||
{
|
||||
AssembledDataObject->SetObjectField(TEXT("camera_state"), PendingFullScene.CameraState);
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> AssembledPayloadObject = MakeShared<FJsonObject>();
|
||||
TSharedRef<FJsonObject> AssembledMetaObject = PendingFullScene.Meta.IsValid()
|
||||
? MakeShared<FJsonObject>(*PendingFullScene.Meta.Get())
|
||||
: MakeShared<FJsonObject>();
|
||||
AssembledMetaObject->SetBoolField(TEXT("chunked"), false);
|
||||
AssembledMetaObject->RemoveField(TEXT("layer_name"));
|
||||
AssembledMetaObject->RemoveField(TEXT("layer_chunk_index"));
|
||||
AssembledMetaObject->RemoveField(TEXT("layer_chunk_count"));
|
||||
AssembledMetaObject->RemoveField(TEXT("transport_chunk_index"));
|
||||
AssembledMetaObject->RemoveField(TEXT("transport_chunk_count"));
|
||||
AssembledPayloadObject->SetObjectField(TEXT("meta"), AssembledMetaObject);
|
||||
|
||||
TSharedRef<FJsonObject> AssembledLayersObject = MakeShared<FJsonObject>();
|
||||
for (TPair<FString, FLayerChunkAccumulator>& LayerPair : PendingFullScene.Layers)
|
||||
{
|
||||
TSharedRef<FJsonObject> LayerObject = MakeShared<FJsonObject>();
|
||||
LayerObject->SetNumberField(TEXT("revision"), LayerPair.Value.Revision);
|
||||
LayerObject->SetArrayField(TEXT("items"), LayerPair.Value.Items);
|
||||
AssembledLayersObject->SetObjectField(LayerPair.Key, LayerObject);
|
||||
}
|
||||
AssembledPayloadObject->SetObjectField(TEXT("layers"), AssembledLayersObject);
|
||||
AssembledDataObject->SetObjectField(TEXT("payload"), AssembledPayloadObject);
|
||||
|
||||
OutAssembledDataObject = AssembledDataObject;
|
||||
PendingFullScene.Reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
void UUeSceneWebSocketClient::BroadcastScenePayload(
|
||||
const FString& UpdateType,
|
||||
int32 Sequence,
|
||||
const TSharedPtr<FJsonObject>& DataObject
|
||||
)
|
||||
{
|
||||
FString PayloadJson;
|
||||
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&PayloadJson);
|
||||
if (FJsonSerializer::Serialize(DataObject.ToSharedRef(), Writer))
|
||||
{
|
||||
OnScenePayload.Broadcast(UpdateType, Sequence, PayloadJson);
|
||||
}
|
||||
|
||||
BroadcastLegacySubmarineCableEvents(DataObject);
|
||||
}
|
||||
|
||||
void UUeSceneWebSocketClient::BroadcastLegacySubmarineCableEvents(
|
||||
const TSharedPtr<FJsonObject>& DataObject
|
||||
)
|
||||
{
|
||||
if (!DataObject.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const TSharedPtr<FJsonObject>* PayloadObject = nullptr;
|
||||
if (!DataObject->TryGetObjectField(TEXT("payload"), PayloadObject)
|
||||
|| PayloadObject == nullptr
|
||||
|| !PayloadObject->IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const TSharedPtr<FJsonObject>* LayersObject = nullptr;
|
||||
if (!(*PayloadObject)->TryGetObjectField(TEXT("layers"), LayersObject)
|
||||
|| LayersObject == nullptr
|
||||
|| !LayersObject->IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const TSharedPtr<FJsonObject>* CableLayerObject = nullptr;
|
||||
if (!(*LayersObject)->TryGetObjectField(TEXT("submarine_cables"), CableLayerObject)
|
||||
|| CableLayerObject == nullptr
|
||||
|| !CableLayerObject->IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const TArray<TSharedPtr<FJsonValue>>* CableItems = nullptr;
|
||||
if (!(*CableLayerObject)->TryGetArrayField(TEXT("items"), CableItems) || CableItems == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (const TSharedPtr<FJsonValue>& CableItemValue : *CableItems)
|
||||
{
|
||||
const TSharedPtr<FJsonObject> CableItemObject = CableItemValue.IsValid()
|
||||
? CableItemValue->AsObject()
|
||||
: nullptr;
|
||||
if (!CableItemObject.IsValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const FString CableName = ResolveCableName(CableItemObject);
|
||||
|
||||
const TSharedPtr<FJsonObject>* GeoObject = nullptr;
|
||||
if (!CableItemObject->TryGetObjectField(TEXT("geo"), GeoObject)
|
||||
|| GeoObject == nullptr
|
||||
|| !GeoObject->IsValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const TArray<TSharedPtr<FJsonValue>>* SegmentArray = nullptr;
|
||||
if ((*GeoObject)->TryGetArrayField(TEXT("segments"), SegmentArray) && SegmentArray != nullptr)
|
||||
{
|
||||
TArray<TArray<FVector>> MergedSegments;
|
||||
for (const TSharedPtr<FJsonValue>& SegmentValue : *SegmentArray)
|
||||
{
|
||||
const TArray<TSharedPtr<FJsonValue>>* SegmentPoints = nullptr;
|
||||
if (!SegmentValue.IsValid() || !SegmentValue->TryGetArray(SegmentPoints) || SegmentPoints == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TArray<FVector> GeoPoints;
|
||||
GeoPoints.Reserve(SegmentPoints->Num());
|
||||
|
||||
for (const TSharedPtr<FJsonValue>& PointValue : *SegmentPoints)
|
||||
{
|
||||
FVector GeoPoint = FVector::ZeroVector;
|
||||
if (PointValue.IsValid() && TryParseGeoPoint(PointValue->AsObject(), GeoPoint))
|
||||
{
|
||||
GeoPoints.Add(GeoPoint);
|
||||
}
|
||||
}
|
||||
|
||||
AppendMergedSegment(MergedSegments, GeoPoints);
|
||||
}
|
||||
|
||||
for (const TArray<FVector>& MergedSegment : MergedSegments)
|
||||
{
|
||||
if (MergedSegment.Num() >= 2)
|
||||
{
|
||||
OnSubmarineCableReceived.Broadcast(CableName, MergedSegment);
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const TArray<TSharedPtr<FJsonValue>>* PathArray = nullptr;
|
||||
if (!(*GeoObject)->TryGetArrayField(TEXT("path"), PathArray) || PathArray == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TArray<FVector> GeoPoints;
|
||||
GeoPoints.Reserve(PathArray->Num());
|
||||
for (const TSharedPtr<FJsonValue>& PointValue : *PathArray)
|
||||
{
|
||||
FVector GeoPoint = FVector::ZeroVector;
|
||||
if (PointValue.IsValid() && TryParseGeoPoint(PointValue->AsObject(), GeoPoint))
|
||||
{
|
||||
GeoPoints.Add(GeoPoint);
|
||||
}
|
||||
}
|
||||
|
||||
if (GeoPoints.Num() >= 2)
|
||||
{
|
||||
OnSubmarineCableReceived.Broadcast(CableName, GeoPoints);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FString UUeSceneWebSocketClient::BuildWsUrl() const
|
||||
{
|
||||
FString WsBase = BackendBaseUrl;
|
||||
WsBase = WsBase.Replace(TEXT("http://"), TEXT("ws://"));
|
||||
WsBase = WsBase.Replace(TEXT("https://"), TEXT("wss://"));
|
||||
return FString::Printf(TEXT("%s/ws?token=%s"), *WsBase, *AccessToken);
|
||||
}
|
||||
|
||||
FString UUeSceneWebSocketClient::BuildHttpUrl(const FString& Path) const
|
||||
{
|
||||
return FString::Printf(TEXT("%s%s"), *BackendBaseUrl, *Path);
|
||||
}
|
||||
|
||||
void UUeSceneWebSocketClient::SendJsonString(const FString& JsonPayload)
|
||||
{
|
||||
if (!Socket.IsValid() || !Socket->IsConnected())
|
||||
{
|
||||
EmitError(TEXT("WebSocket 尚未连接。"));
|
||||
return;
|
||||
}
|
||||
|
||||
Socket->Send(JsonPayload);
|
||||
}
|
||||
159
unreal/Source/Planet/Public/UeSceneWebSocketClient.h
Normal file
159
unreal/Source/Planet/Public/UeSceneWebSocketClient.h
Normal file
@@ -0,0 +1,159 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "Interfaces/IHttpRequest.h"
|
||||
#include "Interfaces/IHttpResponse.h"
|
||||
#include "IWebSocket.h"
|
||||
#include "UeSceneWebSocketClient.generated.h"
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FUeSceneRawMessageEvent, const FString&, Message);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FUeSceneConnectedEvent, bool, bConnected);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FUeSceneErrorEvent, const FString&, ErrorMessage);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(
|
||||
FUeSceneFrameEvent,
|
||||
const FString&,
|
||||
UpdateType,
|
||||
int32,
|
||||
Sequence,
|
||||
int32,
|
||||
TotalRecords
|
||||
);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(
|
||||
FUeScenePayloadEvent,
|
||||
const FString&,
|
||||
UpdateType,
|
||||
int32,
|
||||
Sequence,
|
||||
const FString&,
|
||||
PayloadJson
|
||||
);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(
|
||||
FUeSceneSubmarineCableEvent,
|
||||
const FString&,
|
||||
CableName,
|
||||
const TArray<FVector>&,
|
||||
GeoPoints
|
||||
);
|
||||
|
||||
UCLASS(ClassGroup=(Planet), meta=(BlueprintSpawnableComponent))
|
||||
class PLANET_API UUeSceneWebSocketClient : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UUeSceneWebSocketClient();
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|Backend")
|
||||
FString BackendBaseUrl = TEXT("http://127.0.0.1:8000");
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|Backend")
|
||||
FString Username;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|Backend")
|
||||
FString Password;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|Backend")
|
||||
TArray<FString> DefaultChannels = {TEXT("ue_scene")};
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Planet|Backend")
|
||||
int32 LastSequence = INDEX_NONE;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category="Planet|Backend")
|
||||
FUeSceneConnectedEvent OnConnectedChanged;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category="Planet|Backend")
|
||||
FUeSceneRawMessageEvent OnRawMessage;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category="Planet|Backend")
|
||||
FUeSceneErrorEvent OnError;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category="Planet|Backend")
|
||||
FUeSceneFrameEvent OnSceneFrame;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category="Planet|Backend")
|
||||
FUeScenePayloadEvent OnScenePayload;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category="Planet|Backend|Legacy")
|
||||
FUeSceneSubmarineCableEvent OnSubmarineCableReceived;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="Planet|Backend")
|
||||
void ConnectToBackend();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="Planet|Backend")
|
||||
void DisconnectFromBackend();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="Planet|Backend")
|
||||
void SendSubscribe();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="Planet|Backend")
|
||||
void SendInitialSyncRequest();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="Planet|Backend")
|
||||
void SendSyncRequest(const FString& Reason, int32 InLastSequence);
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
|
||||
|
||||
private:
|
||||
struct FLayerChunkAccumulator
|
||||
{
|
||||
int32 Revision = 0;
|
||||
int32 ExpectedChunkCount = 0;
|
||||
TSet<int32> ReceivedChunkIndices;
|
||||
TArray<TSharedPtr<FJsonValue>> Items;
|
||||
};
|
||||
|
||||
struct FFullSceneAssemblyState
|
||||
{
|
||||
int32 Sequence = INDEX_NONE;
|
||||
int32 ExpectedTransportChunkCount = 0;
|
||||
int32 TotalRecords = 0;
|
||||
FString ClusterTime;
|
||||
TSharedPtr<FJsonObject> DisplayProfile;
|
||||
TSharedPtr<FJsonObject> CameraState;
|
||||
TSharedPtr<FJsonObject> Meta;
|
||||
TMap<FString, FLayerChunkAccumulator> Layers;
|
||||
TSet<int32> ReceivedTransportChunkIndices;
|
||||
|
||||
void Reset()
|
||||
{
|
||||
Sequence = INDEX_NONE;
|
||||
ExpectedTransportChunkCount = 0;
|
||||
TotalRecords = 0;
|
||||
ClusterTime.Reset();
|
||||
DisplayProfile.Reset();
|
||||
CameraState.Reset();
|
||||
Meta.Reset();
|
||||
Layers.Reset();
|
||||
ReceivedTransportChunkIndices.Reset();
|
||||
}
|
||||
};
|
||||
|
||||
FString AccessToken;
|
||||
TSharedPtr<IWebSocket> Socket;
|
||||
FFullSceneAssemblyState PendingFullScene;
|
||||
|
||||
void RequestAccessToken();
|
||||
void HandleLoginResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bSucceeded);
|
||||
void OpenWebSocket();
|
||||
void BindSocketEvents();
|
||||
void EmitError(const FString& ErrorMessage);
|
||||
void TryBroadcastSceneFrame(const FString& Message);
|
||||
bool TryAssembleFullScene(
|
||||
const TSharedPtr<FJsonObject>& DataObject,
|
||||
int32 Sequence,
|
||||
int32 TotalRecords,
|
||||
TSharedPtr<FJsonObject>& OutAssembledDataObject
|
||||
);
|
||||
void BroadcastScenePayload(
|
||||
const FString& UpdateType,
|
||||
int32 Sequence,
|
||||
const TSharedPtr<FJsonObject>& DataObject
|
||||
);
|
||||
void BroadcastLegacySubmarineCableEvents(const TSharedPtr<FJsonObject>& DataObject);
|
||||
FString BuildWsUrl() const;
|
||||
FString BuildHttpUrl(const FString& Path) const;
|
||||
void SendJsonString(const FString& JsonPayload);
|
||||
};
|
||||
Reference in New Issue
Block a user