Add log entry functionality to AIImages. Introduce event description handling in PawnAppearanceData and update prompt generation logic to include event context. Enhance Window_AIImage to display log entries and allow image generation from log events, improving user interaction and character representation.
All checks were successful
SonarQube Analysis / Build and analyze (push) Successful in 1m45s

This commit is contained in:
Leonid Pershin
2025-11-01 14:27:14 +03:00
parent 5c9887c669
commit 59abcb11b8
8 changed files with 468 additions and 9 deletions

Binary file not shown.

View File

@@ -118,4 +118,7 @@
<AIImages.Gallery.AllDeleted>Successfully deleted {0} images</AIImages.Gallery.AllDeleted>
<AIImages.Gallery.OpenGallery>Open gallery</AIImages.Gallery.OpenGallery>
<AIImages.Gallery.ImagesCount>({0})</AIImages.Gallery.ImagesCount>
<!-- Log -->
<AIImages.Log.Entries>Log Events</AIImages.Log.Entries>
<AIImages.Log.GenerateImage>Generate AI Image</AIImages.Log.GenerateImage>
</LanguageData>

View File

@@ -118,4 +118,7 @@
<AIImages.Gallery.AllDeleted>Успешно удалено {0} изображений</AIImages.Gallery.AllDeleted>
<AIImages.Gallery.OpenGallery>Открыть галерею</AIImages.Gallery.OpenGallery>
<AIImages.Gallery.ImagesCount>({0})</AIImages.Gallery.ImagesCount>
<!-- Log -->
<AIImages.Log.Entries>События из журнала</AIImages.Log.Entries>
<AIImages.Log.GenerateImage>Сгенерировать AI изображение</AIImages.Log.GenerateImage>
</LanguageData>

View File

@@ -22,6 +22,7 @@ namespace AIImages.Models
public Color HairColor { get; set; }
public List<Trait> Traits { get; set; }
public List<ApparelData> Apparel { get; set; }
public string EventDescription { get; set; }
public PawnAppearanceData()
{

View File

@@ -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
{
/// <summary>
/// Патч для получения записей лога пешки через ITab_Pawn_Log
/// </summary>
[HarmonyPatch(typeof(ITab_Pawn_Log), "FillTab")]
public static class ITab_Pawn_Log_Patch
{
private static Pawn lastPawn = null;
private static List<LogEntry> cachedEntries = new List<LogEntry>();
[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<LogEntry> 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<LogEntry> 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<LogEntry> 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<LogEntry>();
}
}
/// <summary>
/// Патч для получения записей лога пешки
/// Упрощённая версия - только для получения данных
/// </summary>
public static class PawnLogPatch
{
/// <summary>
/// Получает все записи лога пешки
/// </summary>
public static IEnumerable<LogEntry> GetAllLogEntries(Pawn pawn)
{
// Сначала пробуем получить из кэша ITab_Pawn_Log
var cachedEntries = ITab_Pawn_Log_Patch.GetCachedEntries(pawn);
if (cachedEntries != null && cachedEntries.Any())
{
return cachedEntries;
}
// Затем пробуем другие способы
return GetAllLogEntriesInternal(pawn);
}
/// <summary>
/// Внутренний метод получения записей лога
/// </summary>
private static IEnumerable<LogEntry> GetAllLogEntriesInternal(Pawn pawn)
{
if (pawn == null)
return Enumerable.Empty<LogEntry>();
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<LogEntry> entriesList)
{
return entriesList;
}
}
// Пробуем метод GetEntries
var getEntriesMethod = AccessTools.Method(logs.GetType(), "GetEntries");
if (getEntriesMethod != null)
{
var entries = getEntriesMethod.Invoke(logs, null);
if (entries is IEnumerable<LogEntry> entriesList)
{
return entriesList;
}
}
// Пробуем поле entries
var entriesField = AccessTools.Field(logs.GetType(), "entries");
if (entriesField != null)
{
var entries = entriesField.GetValue(logs);
if (entries is IEnumerable<LogEntry> entriesList)
{
return entriesList;
}
if (entries is System.Collections.IList entriesCollection)
{
return entriesCollection.Cast<LogEntry>();
}
}
}
}
// Способ 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<LogEntry> 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<LogEntry>();
}
}
}

View File

@@ -119,6 +119,33 @@ namespace AIImages.Services
return prompt.ToString().Trim().TrimEnd(',');
}
/// <summary>
/// Генерирует позитивный промпт на основе данных о персонаже и события
/// </summary>
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();

View File

@@ -15,6 +15,15 @@ namespace AIImages.Services
StableDiffusionSettings settings
);
/// <summary>
/// Генерирует позитивный промпт на основе данных о персонаже и события
/// </summary>
string GeneratePositivePromptWithEvent(
PawnAppearanceData appearanceData,
StableDiffusionSettings settings,
string eventDescription
);
/// <summary>
/// Генерирует негативный промпт на основе настроек
/// </summary>

View File

@@ -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; // Развёрнута по умолчанию
/// <summary>
/// Обновляет данные персонажа
@@ -190,6 +193,24 @@ namespace AIImages
/// </summary>
public Pawn CurrentPawn => pawn;
/// <summary>
/// Генерирует изображение для пешки с описанием события
/// </summary>
public void GenerateImageForEvent(string eventDescription)
{
if (pawn == null || isGenerating)
return;
// Устанавливаем описание события
if (appearanceData != null)
{
appearanceData.EventDescription = eventDescription;
}
// Запускаем генерацию
StartGeneration();
}
/// <summary>
/// Отладочный метод для проверки состояния всех пешек
/// </summary>
@@ -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);
}
/// <summary>
@@ -757,13 +792,13 @@ namespace AIImages
/// <summary>
/// Отрисовывает одежду персонажа
/// </summary>
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;
}
/// <summary>
/// Отрисовывает список записей лога пешки
/// </summary>
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<Texture2D>.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);