// Copyright ExByte Studios. Item Interaction Ecosystem. #include "ItemDeveloperModule.h" #include "ItemEcosystemSettings.h" #include "ItemDefinitionRow.h" #include "Engine/DataTable.h" #include "ToolMenus.h" #include "Framework/Notifications/NotificationManager.h" #include "Widgets/Notifications/SNotificationList.h" #include "Misc/FileHelper.h" #include "Misc/Paths.h" DEFINE_LOG_CATEGORY_STATIC(LogItemDeveloper, Log, All); #define LOCTEXT_NAMESPACE "ItemDeveloper" void FItemDeveloperModule::StartupModule() { UToolMenus::RegisterStartupCallback( FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FItemDeveloperModule::RegisterMenus)); } void FItemDeveloperModule::ShutdownModule() { UToolMenus::UnRegisterStartupCallback(this); UToolMenus::UnregisterOwner(this); } void FItemDeveloperModule::RegisterMenus() { FToolMenuOwnerScoped OwnerScoped(this); UToolMenu* ToolsMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Tools"); if (!ToolsMenu) { return; } FToolMenuSection& Section = ToolsMenu->AddSection("ItemEcosystem", LOCTEXT("ItemEcosystemSection", "Item Ecosystem")); Section.AddMenuEntry( "ValidateItemData", LOCTEXT("ValidateItemData", "Validate Item Data"), LOCTEXT("ValidateItemDataTip", "Run data validation over DT_Items (section 28)."), FSlateIcon(), FUIAction(FExecuteAction::CreateStatic(&FItemDeveloperModule::ValidateAllItems))); Section.AddMenuEntry( "ExportItemsCsv", LOCTEXT("ExportItemsCsv", "Export DT_Items to CSV"), LOCTEXT("ExportItemsCsvTip", "Write DT_Items to Saved/ItemEcosystem/DT_Items.csv (section 27)."), FSlateIcon(), FUIAction(FExecuteAction::CreateStatic(&FItemDeveloperModule::ExportItemsToCsv))); Section.AddMenuEntry( "ReportMissingAssets", LOCTEXT("ReportMissingAssets", "Report: Items Missing Icon/Mesh"), LOCTEXT("ReportMissingAssetsTip", "List items with no Icon or WorldMesh (section 27)."), FSlateIcon(), FUIAction(FExecuteAction::CreateStatic(&FItemDeveloperModule::ReportMissingAssets))); } void FItemDeveloperModule::ExportItemsToCsv() { const UItemEcosystemSettings* Settings = UItemEcosystemSettings::Get(); UDataTable* ItemsTable = Settings ? Settings->ItemsTable.LoadSynchronous() : nullptr; if (!ItemsTable) { FNotificationInfo Info(LOCTEXT("NoItemsTable", "DT_Items is not assigned.")); Info.ExpireDuration = 5.f; FSlateNotificationManager::Get().AddNotification(Info); return; } const FString Csv = ItemsTable->GetTableAsCSV(); const FString Path = FPaths::ProjectSavedDir() / TEXT("ItemEcosystem") / TEXT("DT_Items.csv"); const bool bOk = FFileHelper::SaveStringToFile(Csv, *Path); const FText Msg = bOk ? FText::Format(LOCTEXT("ExportOk", "Exported DT_Items to {0}"), FText::FromString(Path)) : LOCTEXT("ExportFail", "Failed to write CSV."); FNotificationInfo Info(Msg); Info.ExpireDuration = 6.f; FSlateNotificationManager::Get().AddNotification(Info); UE_LOG(LogItemDeveloper, Log, TEXT("CSV export: %s -> %s"), bOk ? TEXT("OK") : TEXT("FAIL"), *Path); } void FItemDeveloperModule::ReportMissingAssets() { const UItemEcosystemSettings* Settings = UItemEcosystemSettings::Get(); UDataTable* ItemsTable = Settings ? Settings->ItemsTable.LoadSynchronous() : nullptr; if (!ItemsTable) { return; } int32 Missing = 0; TArray Rows; ItemsTable->GetAllRows(TEXT("ItemDeveloper.ReportMissing"), Rows); for (const FItemDefinitionRow* Row : Rows) { if (!Row) { continue; } if (Row->Icon.IsNull()) { ++Missing; UE_LOG(LogItemDeveloper, Warning, TEXT("%s: no Icon"), *Row->ItemId.ToString()); } if (Row->WorldMesh.IsNull()) { ++Missing; UE_LOG(LogItemDeveloper, Warning, TEXT("%s: no WorldMesh"), *Row->ItemId.ToString()); } } FNotificationInfo Info(FText::Format(LOCTEXT("MissingResult", "Asset report: {0} missing reference(s). See Output Log."), FText::AsNumber(Missing))); Info.ExpireDuration = 6.f; FSlateNotificationManager::Get().AddNotification(Info); } void FItemDeveloperModule::ValidateAllItems() { const UItemEcosystemSettings* Settings = UItemEcosystemSettings::Get(); UDataTable* ItemsTable = Settings ? Settings->ItemsTable.LoadSynchronous() : nullptr; int32 Errors = 0; int32 Warnings = 0; auto Report = [&](const FName& Id, const FString& Msg, bool bError) { bError ? ++Errors : ++Warnings; UE_LOG(LogItemDeveloper, Warning, TEXT("[%s] %s: %s"), bError ? TEXT("ERROR") : TEXT("WARN"), *Id.ToString(), *Msg); }; if (!ItemsTable) { UE_LOG(LogItemDeveloper, Error, TEXT("DT_Items is not assigned in Project Settings -> Item Ecosystem.")); } else { TArray Rows; ItemsTable->GetAllRows(TEXT("ItemDeveloper.Validate"), Rows); for (const FItemDefinitionRow* Row : Rows) { if (!Row) { continue; } const FName Id = Row->ItemId; if (Id.IsNone()) { Report(Id, TEXT("ItemId is empty."), true); } if (Row->MaxStack <= 0) { Report(Id, TEXT("MaxStack <= 0."), true); } if (Row->GridSize.X <= 0 || Row->GridSize.Y <= 0) { Report(Id, TEXT("GridSize must be >= 1."), true); } if (Row->bCanInstantPickup && !Row->bCanStoreInInventory) { Report(Id, TEXT("InstantPickup but not storable."), true); } if (Row->bCanEquip && Row->EquippedActorClass.IsNull()) { Report(Id, TEXT("CanEquip but no EquippedActorClass."), true); } if (Row->bCanWorldCarry && Row->WorldMesh.IsNull()) { Report(Id, TEXT("CanWorldCarry but no WorldMesh."), true); } if (Row->bCanInstallIntoWorldSlot && Row->CompatibleWorldSlots.IsEmpty()) { Report(Id, TEXT("CanInstall but no CompatibleWorldSlots."), true); } if (Row->bIsQuestCritical && Row->bCanDrop) { Report(Id, TEXT("QuestCritical item is droppable."), false); } } UE_LOG(LogItemDeveloper, Log, TEXT("Validated %d item rows."), Rows.Num()); } const FText Summary = FText::Format( LOCTEXT("ValidateResult", "Item validation: {0} error(s), {1} warning(s). See Output Log."), FText::AsNumber(Errors), FText::AsNumber(Warnings)); FNotificationInfo Info(Summary); Info.ExpireDuration = 6.f; FSlateNotificationManager::Get().AddNotification(Info); } #undef LOCTEXT_NAMESPACE IMPLEMENT_MODULE(FItemDeveloperModule, ItemDeveloper)