I3DHConnector.h Source File

instant3Dhub: I3DHConnector.h Source File
instant3Dhub
I3DHConnector.h
Go to the documentation of this file.
1
6#pragma once
7
8#include "CoreMinimal.h"
9
10#include "I3DHConnectorAPI.h"
12#include "I3DHDataTypes.h"
13#include "I3DHVersion.h"
14
15#include "Engine/Texture2D.h"
16#include "GameFramework/Actor.h"
17
18#include "I3DHConnector.generated.h"
19
20class AI3DHGeometry;
22class FI3DHPageToStaticMeshTask;
23class UBodySetup;
24
26DECLARE_DYNAMIC_MULTICAST_DELEGATE(FConnectedDelegate);
28DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FConnectErrorDelegate, const FString&, ErrorMessage);
30DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FErrorDelegate, const FString&, ErrorMessage);
32DECLARE_DYNAMIC_MULTICAST_DELEGATE(FDisconnectedDelegate);
33
43DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FClipPlaneCreatedDelegate, int32, ClipPlaneId, AActor*, ClipPlaneActor, bool, OwnEvent);
44
51DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FClipPlaneRemovedDelegate, int32, ClipPlaneId, bool, OwnEvent);
52
87USTRUCT(BlueprintType, Category = "{instant3Dhub}|Connector")
89{
90 GENERATED_BODY()
91
92
100 UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "{instant3Dhub}|ConnectOptions")
101 FString SessionId;
102
111 UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "{instant3Dhub}|ConnectOptions")
112 FString RestoreSessionId;
113};
114
119UCLASS(ClassGroup = "instant3Dhub", MinimalAPI)
120class AI3DHConnector : public AActor
121{
122 GENERATED_BODY()
123
124public:
125 INSTANT3DHUB_API AI3DHConnector();
126
127
128protected:
129 INSTANT3DHUB_API virtual void BeginPlay() override;
130 INSTANT3DHUB_API virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
131
132 INSTANT3DHUB_API virtual void Tick(float DeltaTime) override;
133
134#if WITH_EDITOR
135 INSTANT3DHUB_API virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
136#endif
137
138 // ----------------------------------------------------------
139 // API Accessor
140 // ----------------------------------------------------------
141public:
149 INSTANT3DHUB_API TSharedPtr<FI3DHConnectorAPI> GetAPI() const;
150
151 // ----------------------------------------------------------
152 // ConnectorAPI
153 // ----------------------------------------------------------
154public:
161 UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = "{instant3Dhub}|Connector")
162 INSTANT3DHUB_API void AddNetworkCredentialCookie(const FString& CookieKey, const FString& CookieValue);
163
169 UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = "{instant3Dhub}|Connector")
170 INSTANT3DHUB_API void RemoveNetworkCredentialCookie(const FString& CookieKey);
171
178 UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = "{instant3Dhub}|Connector")
179 INSTANT3DHUB_API void AddNetworkCredentialToken(const FString& TokenKey, const FString& TokenValue);
180
186 UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = "{instant3Dhub}|Connector")
187 INSTANT3DHUB_API void RemoveNetworkCredentialToken(const FString& TokenKey);
188
194 UFUNCTION(BlueprintCallable, BlueprintPure, Category = "{instant3Dhub}|Connector")
195 INSTANT3DHUB_API const TMap<FString, FString>& GetNetworkCredentialCookies() const { return NetworkCredentialCookies; }
196
202 UFUNCTION(BlueprintCallable, BlueprintPure, Category = "{instant3Dhub}|Connector")
203 INSTANT3DHUB_API const TMap<FString, FString>& GetNetworkCredentialTokens() const { return NetworkCredentialTokens; }
204
244 UFUNCTION(BlueprintCallable, Category = "{instant3Dhub}|Connector")
245 INSTANT3DHUB_API void ConnectToHub(const FString& HubURL, const FString& SessionId);
246
257 UFUNCTION(BlueprintCallable, Category = "{instant3Dhub}|Connector")
258 INSTANT3DHUB_API void ConnectToHubWithOptions(const FString& HubURL, const FI3DHConnectOptions& Options);
259
272 UFUNCTION(BlueprintCallable, Category = "{instant3Dhub}|Connector")
273 INSTANT3DHUB_API void Disconnect();
274
281 UFUNCTION(BlueprintCallable, BlueprintPure, Category = "{instant3Dhub}|Connector")
282 INSTANT3DHUB_API bool IsDisconnected() const;
283
290 UFUNCTION(BlueprintCallable, BlueprintPure, Category = "{instant3Dhub}|Connector")
291 INSTANT3DHUB_API bool IsEstablishingConnectionToHub() const;
292
298 UFUNCTION(BlueprintCallable, BlueprintPure, Category = "{instant3Dhub}|Connector")
299 INSTANT3DHUB_API bool IsConnectedToHub() const;
300
306 UFUNCTION(BlueprintCallable, BlueprintPure, Category = "{instant3Dhub}|Connector")
307 INSTANT3DHUB_API FString GetHubURL() const;
308
314 UFUNCTION(BlueprintCallable, BlueprintPure, Category = "{instant3Dhub}|Connector")
315 INSTANT3DHUB_API FString GetSessionId() const;
316
324 UPROPERTY(BlueprintAssignable, Transient, Category = "{instant3Dhub}|Connector")
325 FConnectedDelegate OnConnectedDelegate;
326
333 UPROPERTY(BlueprintAssignable, Transient, Category = "{instant3Dhub}|Connector")
334 FConnectErrorDelegate OnConnectErrorDelegate;
335
344 UPROPERTY(BlueprintAssignable, Transient, Category = "{instant3Dhub}|Connector")
345 FErrorDelegate OnErrorDelegate;
346
353 UPROPERTY(BlueprintAssignable, Transient, Category = "{instant3Dhub}|Connector")
354 FDisconnectedDelegate OnDisconnectedDelegate;
355
361 UFUNCTION(BlueprintCallable, BlueprintPure, Category = "{instant3Dhub}|Connector")
362 INSTANT3DHUB_API FMatrix GetOriginToWorldTransform() const;
363
369 UFUNCTION(BlueprintCallable, BlueprintPure, Category = "{instant3Dhub}|Connector")
370 INSTANT3DHUB_API FMatrix GetWorldToOriginTransform() const;
371
375 INSTANT3DHUB_API AActor* GetOriginActor()
376 {
377 AActor* RootActor = this;
378 check(RootActor != nullptr);
379 return RootActor;
380 }
381
385 INSTANT3DHUB_API const AActor* GetOriginActor() const
386 {
387 const AActor* RootActor = this;
388 check(RootActor != nullptr);
389 return RootActor;
390 }
391
392
393
395 // ----------------------------------------------------------
396 // Internal Connection Helper Functions
397 // ----------------------------------------------------------
398public:
399 void OnBackendConnected_Internal_GameThread(const FString& SessionId);
400 void OnBackendError_Internal_GameThread(const FString& ErrorMessage);
401private:
402 void ReportError_Internal_GameThread(const FString& ErrorMessage);
403 void ReportConnectError_Internal_GameThread(const FString& ErrorMessage);
404 bool Disconnect_Internal_GameThread();
409 // ----------------------------------------------------------
410 // Debugging Tools
411 // ----------------------------------------------------------
412
413public:
427 UFUNCTION(BlueprintCallable, Category = "{instant3Dhub}|Debugging")
428 INSTANT3DHUB_API bool RequestRemoteCullerDebugStream();
429
442 UFUNCTION(BlueprintCallable, Category = "{instant3Dhub}|Debugging")
443 INSTANT3DHUB_API UTexture2D* GetRemoteCullerDebugStreamTexture();
444
456 UFUNCTION(BlueprintCallable, Category = "{instant3Dhub}|Debugging")
457 INSTANT3DHUB_API const TArray<FI3DHDebugMetric>& GetDebugMetrics();
458
470 UFUNCTION(BlueprintCallable, Category = "{instant3Dhub}|Debugging")
471 INSTANT3DHUB_API FString GetDebugSessionInfo();
472
473 // ----------------------------------------------------------
474 // Progress
475 // ----------------------------------------------------------
476
477public:
489 UFUNCTION(BlueprintCallable, Category = "{instant3Dhub}|Progress")
490 INSTANT3DHUB_API FI3DHClientProgress GetClientProgress();
491
492 // ----------------------------------------------------------
493 // ClipPlane API
494 // ----------------------------------------------------------
495public:
500 UPROPERTY(BlueprintAssignable, Transient, Category = "{instant3Dhub}|Clip Plane API")
501 FClipPlaneCreatedDelegate OnClipPlaneCreatedDelegate;
502
507 UPROPERTY(BlueprintAssignable, Transient, Category = "{instant3Dhub}|Clip Plane API")
508 FClipPlaneRemovedDelegate OnClipPlaneRemovedDelegate;
509
515 UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0.1", ClampMax = "120.0", UIMin = "0.1", UIMax = "120.0"), Category = "{instant3Dhub}|Clip Plane API")
516 float ClipPlaneSyncHz = 1.0;
517
522 UPROPERTY(EditAnywhere, Category = "{instant3Dhub}|Clip Plane API")
523 TSubclassOf<AActor> ClipPlaneActorClass = nullptr;
524
530 UFUNCTION(BlueprintCallable, Category = "{instant3Dhub}|Clip Plane API")
531 INSTANT3DHUB_API TArray<int32> GetClipPlanes();
532
539 UFUNCTION(BlueprintCallable, Category = "{instant3Dhub}|Clip Plane API")
540 INSTANT3DHUB_API AActor* FindClipPlaneActor(int32 ClipPlaneId);
541
547 UFUNCTION(BlueprintCallable, Category = "{instant3Dhub}|Clip Plane API")
548 INSTANT3DHUB_API AActor* GetMainClipPlaneActor();
549
550 // ----------------------------------------------------------
551 // Drawing API
552 // ----------------------------------------------------------
553public:
561 UPROPERTY(EditAnywhere, Category = "{instant3Dhub}|Drawing API", AdvancedDisplay)
562 TObjectPtr<UMaterialInterface> DrawingBaseMaterial{nullptr};
563
571 UPROPERTY(EditAnywhere, Category = "{instant3Dhub}|Drawing API", AdvancedDisplay)
572 TObjectPtr<class UStaticMesh> DrawingSegmentMesh{nullptr};
573
580 UPROPERTY(EditAnywhere, Category = "{instant3Dhub}|Drawing API", AdvancedDisplay)
581 TObjectPtr<class UStaticMesh> DrawingJointMesh{nullptr};
582
590 UFUNCTION(BlueprintCallable, Category = "{instant3Dhub}|Drawing API", meta = (DisplayName = "Create Drawing Material"))
591 INSTANT3DHUB_API UMaterialInterface* GetDrawingMaterial(FLinearColor Color);
592
600 UFUNCTION(BlueprintCallable, Category = "{instant3Dhub}|Drawing API")
601 INSTANT3DHUB_API class AI3DHDrawing* FindDrawingActor(int32 DrawingHandle);
602
609 UFUNCTION(BlueprintCallable, Category = "{instant3Dhub}|Drawing API")
610 INSTANT3DHUB_API void SetClipDrawingsByClipPlane(const bool bClipDrawingsByClipPlaneEnabled);
611
618 UFUNCTION(BlueprintCallable, Category = "{instant3Dhub}|Drawing API")
619 INSTANT3DHUB_API bool GetClipDrawingsByClipPlane();
620
621 // ----------------------------------------------------------
622 // EditDrawing API
623 // ----------------------------------------------------------
624public:
638 UFUNCTION(BlueprintCallable, Category = "{instant3Dhub}|EditDrawing API")
639 virtual void EditDrawing(int32 DrawingHandle, const TScriptInterface<class II3DHEditDrawingInterface>& EditDrawingInterface, bool bIsEditingCopy = false);
640
641 // ----------------------------------------------------------
642 // InstanceGraph API
643 // ----------------------------------------------------------
644public:
645
651 UFUNCTION(BlueprintPure, Category = "{instant3Dhub}|Instance Graph API")
652 INSTANT3DHUB_API int32 GetGlobalRootNodeId();
653
654
655 // ----------------------------------------------------------
656 // Selection API
657 // ----------------------------------------------------------
658public:
665 UFUNCTION(BlueprintCallable, Category = "{instant3Dhub}|Selection API")
666 INSTANT3DHUB_API TArray<int32> GetSelection();
667
668 // ----------------------------------------------------------
669 // TransformAuthority API
670 // ----------------------------------------------------------
671public:
672 INSTANT3DHUB_EXPERIMENTAL(0.0.23, "TransformAuthorityAPI is a temporary solution suspect to changes or replacement.")
681 UFUNCTION(BlueprintCallable, Category = "{instant3Dhub}|Transform Authority", meta = (DisplayName = "Get WorldTransform With Authority (Experimental)"))
682 INSTANT3DHUB_API bool GetWorldTransformWithAuthority(const FI3DHTransformAuthorityHandle TransformAuthorityHandle, int32 NodeId, FMatrix& OutWorldTransform);
683
684 INSTANT3DHUB_EXPERIMENTAL(0.0.23, "TransformAuthorityAPI is a temporary solution suspect to changes or replacement.")
692 UFUNCTION(BlueprintCallable, Category = "{instant3Dhub}|Transform Authority", meta = (DisplayName = "Set WorldTransform With Authority (Experimental)"))
693 INSTANT3DHUB_API void SetWorldTransformWithAuthority(const FI3DHTransformAuthorityHandle TransformAuthorityHandle, int32 NodeId, const FMatrix& WorldTransform);
694
695 INSTANT3DHUB_EXPERIMENTAL(0.0.23, "TransformAuthorityAPI is a temporary solution suspect to changes or replacement.")
702 UFUNCTION(BlueprintCallable, Category = "{instant3Dhub}|Transform Authority", meta = (DisplayName = "Release TransformAuthority (Experimental)"))
703 INSTANT3DHUB_API void ReleaseTransformAuthority(const FI3DHTransformAuthorityHandle TransformAuthorityHandle);
704
705 INSTANT3DHUB_EXPERIMENTAL(0.0.23, "TransformAuthorityAPI is a temporary solution suspect to changes or replacement.")
711 UFUNCTION(BlueprintCallable, Category = "{instant3Dhub}|Transform Authority", meta = (DisplayName = "Is TransformAuthority Active (Experimental)"))
712 INSTANT3DHUB_API bool IsTransformAuthorityActive(const FI3DHTransformAuthorityHandle TransformAuthorityHandle) const;
713
714 // ----------------------------------------------------------
715 // Runtime adjustable performance parameters
716 // ----------------------------------------------------------
717public:
719 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "{instant3Dhub}|Connector", meta = (ClampMin = 1, UIMin = 1))
720 int32 MaxConversionTasksCreatedPerTick = 8;
721
723 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "{instant3Dhub}|Connector", meta = (ClampMin = 1, UIMin = 1))
724 int32 MaxComponentsCreatedPerTick = 2;
725
731 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "{instant3Dhub}|Connector", meta = (ClampMin = 0, UIMin = 0, Delta = 1000000))
732 int32 TargetedTrianglesOnComponentsWithVisibility = 10000000;
733
738 UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0.1", ClampMax = "100.0", UIMin = "0.1", UIMax = "100.0"), Category = "{instant3Dhub}|Connector")
739 float CreateComponentsBudgetMs = 5.0f;
740
745 UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0.1", ClampMax = "120.0", UIMin = "0.1", UIMax = "120.0"), Category = "{instant3Dhub}|Connector")
746 float ViewSyncHz = 5.0;
747
748protected:
753 UPROPERTY(EditAnywhere, Category = "{instant3Dhub}|Connector")
754 TObjectPtr<class UMaterialParameterCollection> I3DHParameterCollection = nullptr;
755
756 PRAGMA_DISABLE_EXPERIMENTAL_WARNINGS
758 UPROPERTY(EditAnywhere, Category = "{instant3Dhub}|Connector")
759 TSubclassOf<AI3DHGeometry> GeometryActorClass;
760 PRAGMA_ENABLE_EXPERIMENTAL_WARNINGS
761
763 UPROPERTY(EditAnywhere, Category = "{instant3Dhub}|Connector")
764 bool bSpawnedGeometryCastsShadow = false;
765
766protected:
768 UPROPERTY(VisibleAnywhere, Category = "{instant3Dhub}|Connector")
769 TObjectPtr<USceneComponent> RootComponentConnector;
770
772 // ----------------------------------------------------------
773 // Internal
774 // ----------------------------------------------------------
775public:
776 FI3DHConnectorDrawingStorage& GetDrawingStorage();
777 const FI3DHConnectorDrawingStorage& GetDrawingStorage() const;
778
779 FI3DHConnectorRenderStorage& GetRenderStorage();
780 const FI3DHConnectorRenderStorage& GetRenderStorage() const;
781
782 const FI3DHConnectorRenderStats& GetRenderStats() const;
783
784 PRAGMA_DISABLE_EXPERIMENTAL_WARNINGS
786 AI3DHGeometry* FindGeometryActor(int32 RootNodeId);
787 PRAGMA_ENABLE_EXPERIMENTAL_WARNINGS
788
789 UMaterialParameterCollectionInstance* GetI3DHParameterCollectionInstance() const;
790
791private:
792 TMap<FString, FString> NetworkCredentialCookies;
793 TMap<FString, FString> NetworkCredentialTokens;
794
795 FString HubURL;
796 FString SessionId;
797
798 // Updated in ConnectToHubWithOptions().
799 // While connected, contains the value of the current connection.
800 bool bUseSenLinBackend = false;
801
802 bool bReloadKeyPressedLastFrame = false;
803 bool bDebugStreamKeyPressedLastFrame = false;
804
805 // Timings for measuring time to first pixel.
806 // All values in seconds of platform time.
807 //
808 // Note that the camera position and orientation can influence the timings
809 // via effects on the remote culler. For accurate measurements the camera
810 // should be in a fixed position pointing at the scene.
811 double ConnectTime;
812 double FirstVisibilityTime;
813 double FirstActorTime;
814
815 // Time estimates for CreateMeshComponents budgeting.
816 // The estimates are updated using exponential moving averages.
817 // For the physics build we assume (based on our tests) that
818 // the constant overhead is negligible and the time is
819 // fully dependent on the per-triangle cost.
820 // Initial values are based on measurements on my computer.
821 double EstimatedSpawnGeometryActorTime = 0.180e-3; // seconds
822 double EstimatedSpawnMeshComponentStaticTime = 0.600e-3; // seconds
823 double EstimatedPhysicsBuildTimePerTriangle = 0.240e-6; // seconds per triangle
824
825 FTimerHandle ViewSyncTimer;
826
827 UPROPERTY()
828 FI3DHConnectorDrawingStorage DrawingStorage;
829
830 UPROPERTY()
831 FI3DHConnectorRenderStorage RenderStorage;
832
833 TArray<FI3DHDebugMetric> DebugMetrics;
834 // Timing metrics in milliseconds.
835 int32 MetricTimingVisibilityMs = 0;
836 int32 MetricTimingGeometryMs = 0;
837
838 // API and Backend
839 // ----------------------------------------------------------
840 TSharedPtr<class FI3DHConnectorAPIImpl> APIImpl;
841
842 TSharedPtr<class FI3DHConnectionPromiseImpl> ConnectionPromise;
843
844 TSharedPtr<class FI3DHConnectorBackend> ConnectorBackend;
845 TSharedPtr<class FI3DHEventBackend> EventBackend;
846 TSharedPtr<class FI3DHRenderBackend> RenderBackend;
847
848private:
849 PRAGMA_DISABLE_EXPERIMENTAL_WARNINGS
857 AI3DHGeometry* SpawnGeometryActor(AActor* AttachActor, int32 RootNodeId, const struct FI3DHInstanceSpawnProperties& SpawnProperties);
858 PRAGMA_ENABLE_EXPERIMENTAL_WARNINGS
859
860 PRAGMA_DISABLE_EXPERIMENTAL_WARNINGS
868 UStaticMeshComponent* SpawnMeshComponentFromTask(FI3DHPageInstanceHandle PageInstanceHandle, AI3DHGeometry* AttachActor, const FI3DHPageToStaticMeshTask& Task, double& PhysicsBuildTime);
869 PRAGMA_ENABLE_EXPERIMENTAL_WARNINGS
870
876 void FinishPhysicsAsyncCook(bool bSuccess, UBodySetup* FinishedBodySetup);
877
878public:
882 void SetAppearance_Internal(const FLinearColor& OverrideColor, EAppearanceURIMode OverrideMode, const TArray<int32>& RootNodeIds);
883
889 void SetTransforms_Internal(const TArray<FMatrix>& GlobalTransforms, const TArray<int32>& RootNodeIds);
890
897 void SetEnabled_Internal(bool bNewEnabled, const TArray<int32>& RootNodeIds);
898
905 void SetVariantEnabled_Internal(bool bNewVariantEnabled, const TArray<int32>& RootNodeIds);
906
912 void SetSelected_Internal(bool bNewSelected, const TArray<int32>& RootNodeIds);
913
914private:
915 PRAGMA_DISABLE_EXPERIMENTAL_WARNINGS
920 void UpdateMeshComponentVisibilityAfterActorHiddenInGameStateChange_Internal(AI3DHGeometry* GeometryActor);
921 PRAGMA_ENABLE_EXPERIMENTAL_WARNINGS
922
923 struct HubCameraParameters
924 {
925 int32 Width;
926 int32 Height;
927 FMatrix44f ViewMatrix;
928 FMatrix44f ProjectionMatrix;
929 };
935 HubCameraParameters GetHubCameraParamsForPlayer(const APlayerController* PlayerController);
936
937 void ViewSyncTimerCallback();
938
939private:
940 TSharedPtr<class FI3DHEventStreamConnector> EventStreamConnector;
941
942private:
943 void AddCredentialHeaders(const FString& URL, TMap<FString, FString>& Headers);
944 void ToggleDebugStreamOverlayModeInternal();
945 void ToggleDebugStreamOverlayEnabledInteral();
946
947 TSharedPtr<class FI3DHDebugStreamConnector> DebugStreamConnector;
948
949 UPROPERTY(Transient)
950 TObjectPtr<class AI3DHDebugStreamOverlay> DebugStreamOverlay;
951
952private:
953 static bool TestIsValidDrawingMaterial(const UMaterialInterface* DrawingMaterial);
954
956};
DECLARE_DYNAMIC_MULTICAST_DELEGATE void FErrorDelegate(const FString &ErrorMessage)
Error Delegate Type.
DECLARE_DYNAMIC_MULTICAST_DELEGATE void FClipPlaneCreatedDelegate(int32 ClipPlaneId, AActor *ClipPlaneActor, bool OwnEvent)
Delegate type invoked when a clip plane is created.
DECLARE_DYNAMIC_MULTICAST_DELEGATE void FDisconnectedDelegate()
Disconnected Delegate Type.
DECLARE_DYNAMIC_MULTICAST_DELEGATE void FClipPlaneRemovedDelegate(int32 ClipPlaneId, bool OwnEvent)
Delegate type invoked when a clip plane is removed.
DECLARE_DYNAMIC_MULTICAST_DELEGATE void FConnectErrorDelegate(const FString &ErrorMessage)
Connect Error Delegate Type.
DECLARE_DYNAMIC_MULTICAST_DELEGATE void FConnectedDelegate()
Connected Delegate Type.
@ Transform
Transform (represented as FMatrix in Unreal)
@ EditDrawing
EditDrawing Mode:
Actor containing mesh components used to represent instant3Dhub geometry.
Definition I3DHGeometry.h:80
Contains optional parameters for connecting to the hub.
Definition I3DHConnector.h:89