diff --git a/Assemblies/AIImages.dll b/Assemblies/AIImages.dll index 0db4f3d..78edded 100644 Binary files a/Assemblies/AIImages.dll and b/Assemblies/AIImages.dll differ diff --git a/Languages/English/Keyed/AIImages.xml b/Languages/English/Keyed/AIImages.xml index 3f4b609..6b94cf2 100644 --- a/Languages/English/Keyed/AIImages.xml +++ b/Languages/English/Keyed/AIImages.xml @@ -118,4 +118,7 @@ Successfully deleted {0} images Open gallery ({0}) + + Log Events + Generate AI Image diff --git a/Languages/Russian/Keyed/AIImages.xml b/Languages/Russian/Keyed/AIImages.xml index 0cd8ad0..15d0079 100644 --- a/Languages/Russian/Keyed/AIImages.xml +++ b/Languages/Russian/Keyed/AIImages.xml @@ -118,4 +118,7 @@ Успешно удалено {0} изображений Открыть галерею ({0}) + + События из журнала + Сгенерировать AI изображение diff --git a/Source/AIImages/Models/PawnAppearanceData.cs b/Source/AIImages/Models/PawnAppearanceData.cs index 4b66f39..a01d579 100644 --- a/Source/AIImages/Models/PawnAppearanceData.cs +++ b/Source/AIImages/Models/PawnAppearanceData.cs @@ -22,6 +22,7 @@ namespace AIImages.Models public Color HairColor { get; set; } public List Traits { get; set; } public List Apparel { get; set; } + public string EventDescription { get; set; } public PawnAppearanceData() { diff --git a/Source/AIImages/Patches/PawnLogPatch.cs b/Source/AIImages/Patches/PawnLogPatch.cs new file mode 100644 index 0000000..73833ff --- /dev/null +++ b/Source/AIImages/Patches/PawnLogPatch.cs @@ -0,0 +1,241 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using HarmonyLib; +using RimWorld; +using UnityEngine; +using Verse; + +namespace AIImages.Patches +{ + /// + /// Патч для получения записей лога пешки через ITab_Pawn_Log + /// + [HarmonyPatch(typeof(ITab_Pawn_Log), "FillTab")] + public static class ITab_Pawn_Log_Patch + { + private static Pawn lastPawn = null; + private static List cachedEntries = new List(); + + [HarmonyPrefix] + public static void Prefix(ITab_Pawn_Log __instance) + { + try + { + // Получаем пешку через рефлексию + var selPawnProp = AccessTools.Property(typeof(ITab), "SelPawn"); + if (selPawnProp != null) + { + var pawn = selPawnProp.GetValue(__instance) as Pawn; + if (pawn != null) + { + // Пробуем получить записи напрямую через Pawn.logs + var logsProperty = AccessTools.Property(typeof(Pawn), "logs"); + if (logsProperty != null) + { + var logs = logsProperty.GetValue(pawn); + if (logs != null) + { + // Пробуем AllEntries + var allEntriesProperty = AccessTools.Property( + logs.GetType(), + "AllEntries" + ); + if (allEntriesProperty != null) + { + var entries = allEntriesProperty.GetValue(logs); + if (entries is IEnumerable entriesList) + { + cachedEntries = entriesList.ToList(); + lastPawn = pawn; + UnityEngine.Debug.Log( + $"[AI Images] Cached {cachedEntries.Count} log entries for {pawn.Name}" + ); + return; + } + } + } + } + } + } + } + catch (System.Exception ex) + { + UnityEngine.Debug.LogWarning( + $"[AI Images] ITab_Pawn_Log patch error: {ex.Message}" + ); + } + } + + public static List GetCachedEntries(Pawn pawn) + { + if (pawn == lastPawn && cachedEntries != null && cachedEntries.Any()) + { + return cachedEntries; + } + + // Если кэш не подходит, пытаемся получить напрямую + if (pawn != null) + { + try + { + var logsProperty = AccessTools.Property(typeof(Pawn), "logs"); + if (logsProperty != null) + { + var logs = logsProperty.GetValue(pawn); + if (logs != null) + { + var allEntriesProperty = AccessTools.Property( + logs.GetType(), + "AllEntries" + ); + if (allEntriesProperty != null) + { + var entries = allEntriesProperty.GetValue(logs); + if (entries is IEnumerable entriesList) + { + var list = entriesList.ToList(); + if (list.Any()) + { + cachedEntries = list; + lastPawn = pawn; + return list; + } + } + } + } + } + } + catch (System.Exception ex) + { + UnityEngine.Debug.LogWarning( + $"[AI Images] Error getting entries directly: {ex.Message}" + ); + } + } + + return new List(); + } + } + + /// + /// Патч для получения записей лога пешки + /// Упрощённая версия - только для получения данных + /// + public static class PawnLogPatch + { + /// + /// Получает все записи лога пешки + /// + public static IEnumerable GetAllLogEntries(Pawn pawn) + { + // Сначала пробуем получить из кэша ITab_Pawn_Log + var cachedEntries = ITab_Pawn_Log_Patch.GetCachedEntries(pawn); + if (cachedEntries != null && cachedEntries.Any()) + { + return cachedEntries; + } + + // Затем пробуем другие способы + return GetAllLogEntriesInternal(pawn); + } + + /// + /// Внутренний метод получения записей лога + /// + private static IEnumerable GetAllLogEntriesInternal(Pawn pawn) + { + if (pawn == null) + return Enumerable.Empty(); + + try + { + // Способ 1: Через property через reflection + var logsProperty = AccessTools.Property(typeof(Pawn), "logs"); + if (logsProperty != null) + { + var logs = logsProperty.GetValue(pawn); + if (logs != null) + { + var allEntriesProperty = AccessTools.Property(logs.GetType(), "AllEntries"); + if (allEntriesProperty != null) + { + var entries = allEntriesProperty.GetValue(logs); + if (entries is IEnumerable entriesList) + { + return entriesList; + } + } + + // Пробуем метод GetEntries + var getEntriesMethod = AccessTools.Method(logs.GetType(), "GetEntries"); + if (getEntriesMethod != null) + { + var entries = getEntriesMethod.Invoke(logs, null); + if (entries is IEnumerable entriesList) + { + return entriesList; + } + } + + // Пробуем поле entries + var entriesField = AccessTools.Field(logs.GetType(), "entries"); + if (entriesField != null) + { + var entries = entriesField.GetValue(logs); + if (entries is IEnumerable entriesList) + { + return entriesList; + } + if (entries is System.Collections.IList entriesCollection) + { + return entriesCollection.Cast(); + } + } + } + } + + // Способ 3: Через story.logs + if (pawn.story != null) + { + var logsField = AccessTools.Field(pawn.story.GetType(), "logs"); + if (logsField != null) + { + var logs = logsField.GetValue(pawn.story); + if (logs != null) + { + var allEntriesProperty = AccessTools.Property( + logs.GetType(), + "AllEntries" + ); + if (allEntriesProperty != null) + { + var entries = allEntriesProperty.GetValue(logs); + if (entries is IEnumerable entriesList) + { + return entriesList; + } + } + } + } + } + } + catch (System.Exception ex) + { + // Логируем ошибку для отладки + UnityEngine.Debug.LogWarning( + $"[AI Images] Error getting log entries for {pawn?.Name}: {ex.Message}" + ); + } + + // Если все способы не сработали, логируем информацию + UnityEngine.Debug.Log( + $"[AI Images] Could not get log entries for {pawn?.Name}. " + + $"logs property exists: {AccessTools.Property(typeof(Pawn), "logs") != null}, " + + $"story property exists: {AccessTools.Property(typeof(Pawn), "story") != null}" + ); + + return Enumerable.Empty(); + } + } +} diff --git a/Source/AIImages/Services/AdvancedPromptGenerator.cs b/Source/AIImages/Services/AdvancedPromptGenerator.cs index 7f334be..2e5ecd8 100644 --- a/Source/AIImages/Services/AdvancedPromptGenerator.cs +++ b/Source/AIImages/Services/AdvancedPromptGenerator.cs @@ -119,6 +119,33 @@ namespace AIImages.Services return prompt.ToString().Trim().TrimEnd(','); } + /// + /// Генерирует позитивный промпт на основе данных о персонаже и события + /// + public string GeneratePositivePromptWithEvent( + PawnAppearanceData appearanceData, + StableDiffusionSettings settings, + string eventDescription + ) + { + if (appearanceData == null) + return "portrait of a person"; + + // Генерируем базовый промпт + string basePrompt = GeneratePositivePrompt(appearanceData, settings); + + // Добавляем описание события, если оно есть + if (!string.IsNullOrEmpty(eventDescription)) + { + StringBuilder prompt = new StringBuilder(basePrompt); + prompt.Append(", "); + prompt.Append(eventDescription.ToLower()); + return prompt.ToString(); + } + + return basePrompt; + } + public string GenerateNegativePrompt(StableDiffusionSettings settings) { StringBuilder negativePrompt = new StringBuilder(); diff --git a/Source/AIImages/Services/IPromptGeneratorService.cs b/Source/AIImages/Services/IPromptGeneratorService.cs index 34e4301..48c1f0d 100644 --- a/Source/AIImages/Services/IPromptGeneratorService.cs +++ b/Source/AIImages/Services/IPromptGeneratorService.cs @@ -15,6 +15,15 @@ namespace AIImages.Services StableDiffusionSettings settings ); + /// + /// Генерирует позитивный промпт на основе данных о персонаже и события + /// + string GeneratePositivePromptWithEvent( + PawnAppearanceData appearanceData, + StableDiffusionSettings settings, + string eventDescription + ); + /// /// Генерирует негативный промпт на основе настроек /// diff --git a/Source/AIImages/Window_AIImage.cs b/Source/AIImages/Window_AIImage.cs index 384aaed..9f5fe07 100644 --- a/Source/AIImages/Window_AIImage.cs +++ b/Source/AIImages/Window_AIImage.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading; using AIImages.Helpers; using AIImages.Models; +using AIImages.Patches; using AIImages.Services; using RimWorld; using UnityEngine; @@ -72,11 +73,13 @@ namespace AIImages private Vector2 mainScrollPosition = Vector2.zero; private Vector2 promptScrollPosition = Vector2.zero; private Vector2 negativePromptScrollPosition = Vector2.zero; + private Vector2 logEntriesScrollPosition = Vector2.zero; private float copiedMessageTime = 0f; // Состояние сворачиваемых секций промптов private bool showPositivePrompt = false; private bool showNegativePrompt = false; + private bool showLogEntries = true; // Развёрнута по умолчанию /// /// Обновляет данные персонажа @@ -190,6 +193,24 @@ namespace AIImages /// public Pawn CurrentPawn => pawn; + /// + /// Генерирует изображение для пешки с описанием события + /// + public void GenerateImageForEvent(string eventDescription) + { + if (pawn == null || isGenerating) + return; + + // Устанавливаем описание события + if (appearanceData != null) + { + appearanceData.EventDescription = eventDescription; + } + + // Запускаем генерацию + StartGeneration(); + } + /// /// Отладочный метод для проверки состояния всех пешек /// @@ -307,11 +328,24 @@ namespace AIImages try { - // Генерируем промпты - string positivePrompt = promptGeneratorService.GeneratePositivePrompt( - appearanceData, - generationSettings - ); + // Генерируем промпты - если есть описание события, используем его + string positivePrompt; + if (!string.IsNullOrEmpty(appearanceData?.EventDescription)) + { + positivePrompt = promptGeneratorService.GeneratePositivePromptWithEvent( + appearanceData, + generationSettings, + appearanceData.EventDescription + ); + } + else + { + positivePrompt = promptGeneratorService.GeneratePositivePrompt( + appearanceData, + generationSettings + ); + } + string negativePrompt = promptGeneratorService.GenerateNegativePrompt( generationSettings ); @@ -575,7 +609,8 @@ namespace AIImages contentY = DrawTraits(parentRect, contentY, lineHeight); contentY = DrawGenes(parentRect, contentY, lineHeight); contentY = DrawHediffs(parentRect, contentY, lineHeight); - DrawApparel(parentRect, contentY, lineHeight); + contentY = DrawApparel(parentRect, contentY, lineHeight); + contentY = DrawLogEntries(parentRect, contentY, lineHeight); } /// @@ -757,13 +792,13 @@ namespace AIImages /// /// Отрисовывает одежду персонажа /// - private void DrawApparel(Rect parentRect, float startY, float lineHeight) + private float DrawApparel(Rect parentRect, float startY, float lineHeight) { float contentY = startY; var apparel = pawn.apparel?.WornApparel; if (apparel == null || !apparel.Any()) - return; + return contentY; contentY += 15f; Text.Font = GameFont.Small; @@ -785,6 +820,141 @@ namespace AIImages ); contentY += apparelHeight; } + + return contentY; + } + + /// + /// Отрисовывает список записей лога пешки + /// + private float DrawLogEntries(Rect parentRect, float startY, float lineHeight) + { + float contentY = startY; + + // Получаем записи лога + var entries = PawnLogPatch.GetAllLogEntries(pawn).ToList(); + int entriesCount = entries?.Count ?? 0; + + // Отладочная информация (временно) + if (entriesCount == 0 && pawn != null) + { + // Пробуем проверить, есть ли вообще доступ к логам + try + { + var logsProp = HarmonyLib.AccessTools.Property(typeof(Pawn), "logs"); + var hasLogs = logsProp != null; + + // Выводим в консоль для отладки + DebugLogger.Log( + $"[AI Images] Log entries count: {entriesCount}, Has logs property: {hasLogs}, Pawn: {pawn?.Name}" + ); + } + catch (System.Exception ex) + { + DebugLogger.Warning($"[AI Images] Debug error: {ex.Message}"); + } + } + + contentY += 15f; + + // Заголовок секции - всегда показываем, даже если записей нет + Text.Font = GameFont.Small; + Rect headerRect = new Rect( + parentRect.x + 5f, + contentY, + parentRect.width - 10f, + lineHeight + 5f + ); + + // Кнопка сворачивания/разворачивания + string headerText = "AIImages.Log.Entries".Translate() + $" ({entriesCount})"; + if (Widgets.ButtonText(headerRect, headerText)) + { + showLogEntries = !showLogEntries; + } + contentY += lineHeight + 10f; + + // Разделитель + Widgets.DrawLineHorizontal(parentRect.x, contentY, parentRect.width); + contentY += 10f; + + // Список записей (если развёрнуто и есть записи) + if (showLogEntries && entries != null && entries.Any()) + { + float entryWidth = parentRect.width - 25f; + float scrollViewHeight = Mathf.Min(entries.Count * 30f + 20f, 300f); + + Rect scrollViewRect = new Rect( + parentRect.x + 5f, + contentY, + entryWidth + 15f, + scrollViewHeight + ); + + Rect viewRect = new Rect(0f, 0f, entryWidth, entries.Count * 30f); + + Widgets.BeginScrollView(scrollViewRect, ref logEntriesScrollPosition, viewRect); + + Text.Font = GameFont.Tiny; + float entryY = 0f; + const float entryHeight = 28f; + const float buttonSize = 20f; + + foreach (var entry in entries.Take(50)) // Ограничиваем для производительности + { + string entryText = entry.ToGameStringFromPOV(pawn); + if (string.IsNullOrEmpty(entryText)) + continue; + + // Обрезаем длинный текст + string shortText = + entryText.Length > 60 ? entryText.Substring(0, 57) + "..." : entryText; + + Rect entryRect = new Rect( + 0f, + entryY, + entryWidth - buttonSize - 5f, + entryHeight + ); + Rect buttonRect = new Rect( + entryWidth - buttonSize, + entryY + (entryHeight - buttonSize) / 2f, + buttonSize, + buttonSize + ); + + // Фон записи + Widgets.DrawBoxSolid(entryRect, new Color(0.15f, 0.15f, 0.15f, 0.3f)); + + // Текст записи + Widgets.Label(entryRect.ContractedBy(3f), shortText); + + // Кнопка генерации изображения + Texture2D iconTexture = ContentFinder.Get( + "UI/Commands/AIImage", + true + ); + if (iconTexture != null) + { + if (Widgets.ButtonImage(buttonRect, iconTexture)) + { + GenerateImageForEvent(entryText); + } + + TooltipHandler.TipRegion( + buttonRect, + "AIImages.Log.GenerateImage".Translate() + ); + } + + entryY += entryHeight; + } + + Widgets.EndScrollView(); + contentY += scrollViewHeight + 10f; + } + + return contentY; } private void DrawRightColumn(Rect rect) @@ -1311,7 +1481,12 @@ namespace AIImages galleryLabel += " " + "AIImages.Gallery.ImagesCount".Translate(imageCount); } - if (Widgets.ButtonText(new Rect(galleryButtonX, curY, galleryButtonWidth, 35f), galleryLabel)) + if ( + Widgets.ButtonText( + new Rect(galleryButtonX, curY, galleryButtonWidth, 35f), + galleryLabel + ) + ) { var galleryWindow = new Window_AIGallery(pawn); Find.WindowStack.Add(galleryWindow);