rusteal 0.8.0

Rust bindings for Unreal Engine 5.8.2
#include "RustealAutoReload.h"

#include "DirectoryWatcherModule.h"
#include "Framework/Application/SlateApplication.h"
#include "Framework/Notifications/NotificationManager.h"
#include "HAL/FileManager.h"
#include "HAL/IConsoleManager.h"
#include "IDirectoryWatcher.h"
#include "Misc/Paths.h"
#include "Modules/ModuleManager.h"
#include "RustealLibraries.h"
#include "Widgets/Notifications/SNotificationList.h"

#define LOCTEXT_NAMESPACE "RustealAutoReload"

DEFINE_LOG_CATEGORY_STATIC(LogRustealAutoReload, Log, All);

static TAutoConsoleVariable<bool> CVarAutoReload(
    TEXT("Rusteal.AutoReload"), true,
    TEXT("Hot reload a Rust library as soon as `rusteal build` deploys it "
         "again (1, the default), or only with Rusteal.Reload (0)."));

static constexpr double SettleSeconds = 0.5;

static FString FullPath(const FString &Path) {
  FString Full = FPaths::ConvertRelativePathToFull(Path);
  FPaths::NormalizeFilename(Full);
  return Full;
}

static IDirectoryWatcher *DirectoryWatcher() {
  FDirectoryWatcherModule &Module =
      FModuleManager::LoadModuleChecked<FDirectoryWatcherModule>(
          TEXT("DirectoryWatcher"));

  return Module.Get();
}

static void Notify(const FText &Text, bool bSucceeded) {
  if (!FSlateApplication::IsInitialized()) {
    return;
  }

  FNotificationInfo Info(Text);
  Info.ExpireDuration = bSucceeded ? 3.0f : 6.0f;
  Info.bFireAndForget = true;

  if (TSharedPtr<SNotificationItem> Item =
          FSlateNotificationManager::Get().AddNotification(Info)) {
    Item->SetCompletionState(bSucceeded ? SNotificationItem::CS_Success
                                        : SNotificationItem::CS_Fail);
  }
}

void FRustealAutoReload::Start() {
  RustealForEachLibrary([this](FName Name, const FString &DeployedPath) {
    Watch(Name, DeployedPath);
  });

  RegisteredHandle = RustealOnLibraryRegistered().AddLambda(
      [this](FName Name, const FString &DeployedPath) {
        Watch(Name, DeployedPath);
      });

  TickerHandle = FTSTicker::GetCoreTicker().AddTicker(
      FTickerDelegate::CreateRaw(this, &FRustealAutoReload::Tick));
}

void FRustealAutoReload::Stop() {
  FTSTicker::GetCoreTicker().RemoveTicker(TickerHandle);
  RustealOnLibraryRegistered().Remove(RegisteredHandle);

  if (FModuleManager::Get().IsModuleLoaded(TEXT("DirectoryWatcher"))) {
    IDirectoryWatcher *Watcher = DirectoryWatcher();

    for (const FWatchedLibrary &Library : Libraries) {
      if (Watcher && Library.WatchHandle.IsValid()) {
        Watcher->UnregisterDirectoryChangedCallback_Handle(Library.Directory,
                                                           Library.WatchHandle);
      }
    }
  }

  Libraries.Empty();
}

void FRustealAutoReload::Watch(FName Name, const FString &DeployedPath) {
  if (DeployedPath.IsEmpty() ||
      Libraries.ContainsByPredicate([Name](const FWatchedLibrary &Library) {
        return Library.Name == Name;
      })) {
    return;
  }

  IDirectoryWatcher *Watcher = DirectoryWatcher();

  if (!Watcher) {
    return;
  }

  FWatchedLibrary Library;
  Library.Name = Name;
  Library.File = FullPath(DeployedPath);
  Library.Directory = FPaths::GetPath(Library.File);

  IFileManager::Get().MakeDirectory(*Library.Directory, true);

  Watcher->RegisterDirectoryChangedCallback_Handle(
      Library.Directory,
      IDirectoryWatcher::FDirectoryChanged::CreateRaw(
          this, &FRustealAutoReload::OnDirectoryChanged, Name),
      Library.WatchHandle);

  UE_LOG(LogRustealAutoReload, Display, TEXT("Watching %s (%s)"), *Library.File,
         *Name.ToString());

  Libraries.Add(MoveTemp(Library));
}

void FRustealAutoReload::OnDirectoryChanged(
    const TArray<FFileChangeData> &Changes, FName Name) {
  FWatchedLibrary *Library = Libraries.FindByPredicate(
      [Name](const FWatchedLibrary &Watched) { return Watched.Name == Name; });

  if (!Library || !CVarAutoReload.GetValueOnGameThread()) {
    return;
  }

  for (const FFileChangeData &Change : Changes) {
    if (Change.Action != FFileChangeData::FCA_Removed &&
        FPaths::IsSamePath(FullPath(Change.Filename), Library->File)) {
      Library->ChangedAt = FPlatformTime::Seconds();
    }
  }
}

bool FRustealAutoReload::Tick(float DeltaSeconds) {
  const double Now = FPlatformTime::Seconds();

  for (FWatchedLibrary &Library : Libraries) {
    if (Library.ChangedAt == 0.0 || Now - Library.ChangedAt < SettleSeconds ||
        !FPaths::FileExists(Library.File)) {
      continue;
    }

    Library.ChangedAt = 0.0;

    UE_LOG(LogRustealAutoReload, Display, TEXT("%s was deployed again"),
           *Library.File);

    const bool bReloaded = RustealReloadLibrary(Library.Name);

    Notify(FText::Format(bReloaded
                             ? LOCTEXT("Reloaded", "Rusteal: {0} reloaded")
                             : LOCTEXT("ReloadFailed",
                                       "Rusteal: {0} failed to reload, see the "
                                       "Output Log"),
                         FText::FromName(Library.Name)),
           bReloaded);
  }

  return true;
}

#undef LOCTEXT_NAMESPACE