Files
planet/unreal/Source/Planet/Private/UeSceneWebSocketClient.cpp
2026-05-09 15:51:46 +08:00

678 lines
21 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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);
}