Compare commits
2 Commits
0f60721162
...
2af1ef9292
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2af1ef9292 | ||
|
|
6715544952 |
Binary file not shown.
@@ -45,10 +45,16 @@
|
||||
<AIImages.Settings.ApiEndpoint>API Endpoint</AIImages.Settings.ApiEndpoint>
|
||||
<AIImages.Settings.TestConnection>Test Connection</AIImages.Settings.TestConnection>
|
||||
<AIImages.Settings.LoadModels>Load Available Models</AIImages.Settings.LoadModels>
|
||||
<AIImages.Settings.Model>Model</AIImages.Settings.Model>
|
||||
<AIImages.Settings.NoModelSelected>No model selected</AIImages.Settings.NoModelSelected>
|
||||
<AIImages.Settings.LoadModelsFirst>Load models first</AIImages.Settings.LoadModelsFirst>
|
||||
<AIImages.Settings.LoadSamplersSchedulers>Load Samplers & Schedulers</AIImages.Settings.LoadSamplersSchedulers>
|
||||
<AIImages.Settings.ConnectionSuccess>Successfully connected to API!</AIImages.Settings.ConnectionSuccess>
|
||||
<AIImages.Settings.ConnectionFailed>Failed to connect to API. Check endpoint and ensure Stable Diffusion WebUI is running.</AIImages.Settings.ConnectionFailed>
|
||||
<AIImages.Settings.ModelsLoaded>Loaded {0} models from API</AIImages.Settings.ModelsLoaded>
|
||||
<AIImages.Settings.NoModelsFound>No models found. Check API connection.</AIImages.Settings.NoModelsFound>
|
||||
<AIImages.Settings.SamplersSchedulersLoaded>Loaded {0} samplers and {1} schedulers from API</AIImages.Settings.SamplersSchedulersLoaded>
|
||||
<AIImages.Settings.NoSamplersSchedulersFound>No samplers or schedulers found. Check API connection.</AIImages.Settings.NoSamplersSchedulersFound>
|
||||
<AIImages.Settings.GenerationSection>Generation Settings</AIImages.Settings.GenerationSection>
|
||||
<AIImages.Settings.GenerationSectionTooltip>Configure image generation parameters</AIImages.Settings.GenerationSectionTooltip>
|
||||
<AIImages.Settings.ArtStyle>Art Style</AIImages.Settings.ArtStyle>
|
||||
@@ -58,6 +64,7 @@
|
||||
<AIImages.Settings.Width>Width</AIImages.Settings.Width>
|
||||
<AIImages.Settings.Height>Height</AIImages.Settings.Height>
|
||||
<AIImages.Settings.Sampler>Sampler</AIImages.Settings.Sampler>
|
||||
<AIImages.Settings.Scheduler>Schedule Type</AIImages.Settings.Scheduler>
|
||||
<AIImages.Settings.PromptsSection>Prompts</AIImages.Settings.PromptsSection>
|
||||
<AIImages.Settings.PromptsSectionTooltip>Base prompts that will be added to all generations</AIImages.Settings.PromptsSectionTooltip>
|
||||
<AIImages.Settings.BasePositivePrompt>Base Positive Prompt</AIImages.Settings.BasePositivePrompt>
|
||||
|
||||
@@ -45,10 +45,16 @@
|
||||
<AIImages.Settings.ApiEndpoint>Адрес API</AIImages.Settings.ApiEndpoint>
|
||||
<AIImages.Settings.TestConnection>Проверить соединение</AIImages.Settings.TestConnection>
|
||||
<AIImages.Settings.LoadModels>Загрузить доступные модели</AIImages.Settings.LoadModels>
|
||||
<AIImages.Settings.Model>Модель</AIImages.Settings.Model>
|
||||
<AIImages.Settings.NoModelSelected>Модель не выбрана</AIImages.Settings.NoModelSelected>
|
||||
<AIImages.Settings.LoadModelsFirst>Сначала загрузите модели</AIImages.Settings.LoadModelsFirst>
|
||||
<AIImages.Settings.LoadSamplersSchedulers>Загрузить сэмплеры и планировщики</AIImages.Settings.LoadSamplersSchedulers>
|
||||
<AIImages.Settings.ConnectionSuccess>Успешное подключение к API!</AIImages.Settings.ConnectionSuccess>
|
||||
<AIImages.Settings.ConnectionFailed>Не удалось подключиться к API. Проверьте адрес и убедитесь, что Stable Diffusion WebUI запущен.</AIImages.Settings.ConnectionFailed>
|
||||
<AIImages.Settings.ModelsLoaded>Загружено {0} моделей из API</AIImages.Settings.ModelsLoaded>
|
||||
<AIImages.Settings.NoModelsFound>Модели не найдены. Проверьте подключение к API.</AIImages.Settings.NoModelsFound>
|
||||
<AIImages.Settings.SamplersSchedulersLoaded>Загружено {0} сэмплеров и {1} планировщиков из API</AIImages.Settings.SamplersSchedulersLoaded>
|
||||
<AIImages.Settings.NoSamplersSchedulersFound>Сэмплеры и планировщики не найдены. Проверьте подключение к API.</AIImages.Settings.NoSamplersSchedulersFound>
|
||||
<AIImages.Settings.GenerationSection>Настройки генерации</AIImages.Settings.GenerationSection>
|
||||
<AIImages.Settings.GenerationSectionTooltip>Настройка параметров генерации изображений</AIImages.Settings.GenerationSectionTooltip>
|
||||
<AIImages.Settings.ArtStyle>Художественный стиль</AIImages.Settings.ArtStyle>
|
||||
@@ -58,6 +64,7 @@
|
||||
<AIImages.Settings.Width>Ширина</AIImages.Settings.Width>
|
||||
<AIImages.Settings.Height>Высота</AIImages.Settings.Height>
|
||||
<AIImages.Settings.Sampler>Сэмплер</AIImages.Settings.Sampler>
|
||||
<AIImages.Settings.Scheduler>Тип планировщика</AIImages.Settings.Scheduler>
|
||||
<AIImages.Settings.PromptsSection>Промпты</AIImages.Settings.PromptsSection>
|
||||
<AIImages.Settings.PromptsSectionTooltip>Базовые промпты, которые будут добавлены ко всем генерациям</AIImages.Settings.PromptsSectionTooltip>
|
||||
<AIImages.Settings.BasePositivePrompt>Базовый позитивный промпт</AIImages.Settings.BasePositivePrompt>
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace AIImages.Models
|
||||
public int Width { get; set; }
|
||||
public int Height { get; set; }
|
||||
public string Sampler { get; set; }
|
||||
public string Scheduler { get; set; }
|
||||
public int Seed { get; set; }
|
||||
public string Model { get; set; }
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace AIImages.Models
|
||||
public string BodyType { get; set; }
|
||||
public Color SkinColor { get; set; }
|
||||
public string HairStyle { get; set; }
|
||||
public string HairDefName { get; set; }
|
||||
public Color HairColor { get; set; }
|
||||
public List<Trait> Traits { get; set; }
|
||||
public List<ApparelData> Apparel { get; set; }
|
||||
@@ -33,7 +34,9 @@ namespace AIImages.Models
|
||||
public class ApparelData
|
||||
{
|
||||
public string Label { get; set; }
|
||||
public string DefName { get; set; }
|
||||
public string Material { get; set; }
|
||||
public string MaterialDefName { get; set; }
|
||||
public QualityCategory? Quality { get; set; }
|
||||
public Color Color { get; set; }
|
||||
public string LayerType { get; set; }
|
||||
|
||||
@@ -12,10 +12,10 @@ namespace AIImages.Models
|
||||
public int Width { get; set; }
|
||||
public int Height { get; set; }
|
||||
public string Sampler { get; set; }
|
||||
public string Scheduler { get; set; }
|
||||
public int Seed { get; set; }
|
||||
public string Model { get; set; }
|
||||
public ArtStyle ArtStyle { get; set; }
|
||||
public ShotType ShotType { get; set; }
|
||||
|
||||
public StableDiffusionSettings()
|
||||
{
|
||||
@@ -25,9 +25,9 @@ namespace AIImages.Models
|
||||
Width = 512;
|
||||
Height = 768;
|
||||
Sampler = "Euler a";
|
||||
Scheduler = "Automatic";
|
||||
Seed = -1; // Случайный seed
|
||||
ArtStyle = ArtStyle.Realistic;
|
||||
ShotType = ShotType.Portrait;
|
||||
PositivePrompt = "";
|
||||
NegativePrompt = "ugly, deformed, low quality, blurry, bad anatomy, worst quality";
|
||||
}
|
||||
@@ -38,6 +38,7 @@ namespace AIImages.Models
|
||||
/// </summary>
|
||||
public enum ArtStyle
|
||||
{
|
||||
None, // Без стиля
|
||||
Realistic,
|
||||
SemiRealistic,
|
||||
Anime,
|
||||
@@ -47,16 +48,4 @@ namespace AIImages.Models
|
||||
Sketch,
|
||||
CellShaded,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Тип кадра/композиции
|
||||
/// </summary>
|
||||
public enum ShotType
|
||||
{
|
||||
Portrait, // Портрет (голова и плечи)
|
||||
HalfBody, // Половина тела
|
||||
FullBody, // Полное тело
|
||||
CloseUp, // Крупный план
|
||||
ThreeQuarter, // Три четверти
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ namespace AIImages.Services
|
||||
string
|
||||
>
|
||||
{
|
||||
{ ArtStyle.None, "" },
|
||||
{ ArtStyle.Realistic, "photorealistic, hyperrealistic, realistic photo, photography" },
|
||||
{ ArtStyle.SemiRealistic, "semi-realistic, detailed illustration, realistic art" },
|
||||
{ ArtStyle.Anime, "anime style, manga style, anime character" },
|
||||
@@ -52,18 +53,6 @@ namespace AIImages.Services
|
||||
{ ArtStyle.CellShaded, "cell shaded, flat colors, toon shading, stylized" },
|
||||
};
|
||||
|
||||
private static readonly Dictionary<ShotType, string> ShotTypePrompts = new Dictionary<
|
||||
ShotType,
|
||||
string
|
||||
>
|
||||
{
|
||||
{ ShotType.Portrait, "portrait, head and shoulders" },
|
||||
{ ShotType.HalfBody, "half body shot, waist up" },
|
||||
{ ShotType.FullBody, "full body, full length" },
|
||||
{ ShotType.CloseUp, "close up, face focus, detailed face" },
|
||||
{ ShotType.ThreeQuarter, "three-quarter view, 3/4 view" },
|
||||
};
|
||||
|
||||
public string GeneratePositivePrompt(
|
||||
PawnAppearanceData appearanceData,
|
||||
StableDiffusionSettings settings
|
||||
@@ -75,18 +64,17 @@ namespace AIImages.Services
|
||||
StringBuilder prompt = new StringBuilder();
|
||||
|
||||
// 1. Художественный стиль
|
||||
if (ArtStylePrompts.TryGetValue(settings.ArtStyle, out string stylePrompt))
|
||||
if (
|
||||
ArtStylePrompts.TryGetValue(settings.ArtStyle, out string stylePrompt)
|
||||
&& !string.IsNullOrEmpty(stylePrompt)
|
||||
)
|
||||
{
|
||||
prompt.Append(stylePrompt);
|
||||
prompt.Append(", ");
|
||||
}
|
||||
|
||||
// 2. Тип кадра
|
||||
if (ShotTypePrompts.TryGetValue(settings.ShotType, out string shotPrompt))
|
||||
{
|
||||
prompt.Append(shotPrompt);
|
||||
prompt.Append(" of ");
|
||||
}
|
||||
// 2. Тип кадра - автоматически добавляем "portrait" для генерации персонажей
|
||||
prompt.Append("portrait, head and shoulders of ");
|
||||
|
||||
// 3. Базовое описание (возраст и пол)
|
||||
prompt.Append(GetAgeAndGenderDescription(appearanceData));
|
||||
@@ -157,16 +145,18 @@ namespace AIImages.Services
|
||||
);
|
||||
|
||||
// Специфичные для стиля негативы
|
||||
if (
|
||||
settings.ArtStyle == ArtStyle.Realistic
|
||||
|| settings.ArtStyle == ArtStyle.SemiRealistic
|
||||
)
|
||||
switch (settings.ArtStyle)
|
||||
{
|
||||
negativePrompt.Append("cartoon, anime, painting, drawing, illustration, ");
|
||||
}
|
||||
else if (settings.ArtStyle == ArtStyle.Anime)
|
||||
{
|
||||
negativePrompt.Append("realistic, photo, photography, 3d, ");
|
||||
case ArtStyle.Realistic:
|
||||
case ArtStyle.SemiRealistic:
|
||||
negativePrompt.Append("cartoon, anime, painting, drawing, illustration, ");
|
||||
break;
|
||||
case ArtStyle.Anime:
|
||||
negativePrompt.Append("realistic, photo, photography, 3d, ");
|
||||
break;
|
||||
case ArtStyle.None:
|
||||
// Без дополнительных негативных промптов для стиля None
|
||||
break;
|
||||
}
|
||||
|
||||
// Пользовательский негативный промпт
|
||||
@@ -244,7 +234,7 @@ namespace AIImages.Services
|
||||
|
||||
private string GetHairDescription(PawnAppearanceData data)
|
||||
{
|
||||
if (string.IsNullOrEmpty(data.HairStyle))
|
||||
if (string.IsNullOrEmpty(data.HairDefName))
|
||||
return "";
|
||||
|
||||
StringBuilder hair = new StringBuilder();
|
||||
@@ -254,10 +244,8 @@ namespace AIImages.Services
|
||||
hair.Append(hairColor);
|
||||
hair.Append(" ");
|
||||
|
||||
// Стиль прически (упрощаем сложные названия)
|
||||
string style = data
|
||||
.HairStyle.ToLower()
|
||||
.Replace("_", " ")
|
||||
// Стиль прически - используем DefName для английского названия
|
||||
string style = CleanDefName(data.HairDefName)
|
||||
.Replace("shaved", "very short")
|
||||
.Replace("mohawk", "mohawk hairstyle");
|
||||
|
||||
@@ -310,15 +298,15 @@ namespace AIImages.Services
|
||||
itemDesc.Append(" ");
|
||||
}
|
||||
|
||||
// Материал
|
||||
if (!string.IsNullOrEmpty(item.Material))
|
||||
// Материал - используем DefName для английского названия
|
||||
if (!string.IsNullOrEmpty(item.MaterialDefName))
|
||||
{
|
||||
itemDesc.Append(item.Material.ToLower());
|
||||
itemDesc.Append(CleanDefName(item.MaterialDefName));
|
||||
itemDesc.Append(" ");
|
||||
}
|
||||
|
||||
// Название предмета
|
||||
itemDesc.Append(item.Label.ToLower());
|
||||
// Название предмета - используем DefName для английского названия
|
||||
itemDesc.Append(CleanDefName(item.DefName));
|
||||
|
||||
items.Add(itemDesc.ToString());
|
||||
}
|
||||
@@ -332,22 +320,56 @@ namespace AIImages.Services
|
||||
{
|
||||
var baseTags = "highly detailed, professional, masterpiece, best quality";
|
||||
|
||||
if (style == ArtStyle.Realistic || style == ArtStyle.SemiRealistic)
|
||||
switch (style)
|
||||
{
|
||||
return $"{baseTags}, professional photography, 8k uhd, dslr, high quality, sharp focus";
|
||||
}
|
||||
else if (style == ArtStyle.Anime)
|
||||
{
|
||||
return $"{baseTags}, anime masterpiece, high resolution, vibrant colors";
|
||||
}
|
||||
else if (style == ArtStyle.ConceptArt)
|
||||
{
|
||||
return $"{baseTags}, trending on artstation, professional digital art";
|
||||
}
|
||||
else
|
||||
{
|
||||
return baseTags;
|
||||
case ArtStyle.None:
|
||||
return baseTags;
|
||||
case ArtStyle.Realistic:
|
||||
case ArtStyle.SemiRealistic:
|
||||
return $"{baseTags}, professional photography, 8k uhd, dslr, high quality, sharp focus";
|
||||
case ArtStyle.Anime:
|
||||
return $"{baseTags}, anime masterpiece, high resolution, vibrant colors";
|
||||
case ArtStyle.ConceptArt:
|
||||
return $"{baseTags}, trending on artstation, professional digital art";
|
||||
default:
|
||||
return baseTags;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Преобразует defName в читаемый английский текст для промпта
|
||||
/// Пример: "Apparel_Pants" -> "pants", "Synthread" -> "synthread"
|
||||
/// </summary>
|
||||
private string CleanDefName(string defName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(defName))
|
||||
return "";
|
||||
|
||||
string cleaned = defName;
|
||||
|
||||
// Убираем распространенные префиксы RimWorld
|
||||
cleaned = System.Text.RegularExpressions.Regex.Replace(
|
||||
cleaned,
|
||||
"^(Apparel_|Armor_|Weapon_|Thing_)",
|
||||
"",
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase
|
||||
);
|
||||
|
||||
// Разделяем CamelCase на слова
|
||||
cleaned = System.Text.RegularExpressions.Regex.Replace(
|
||||
cleaned,
|
||||
"([a-z])([A-Z])",
|
||||
"$1 $2"
|
||||
);
|
||||
|
||||
// Заменяем подчеркивания на пробелы
|
||||
cleaned = cleaned.Replace("_", " ");
|
||||
|
||||
// Убираем множественные пробелы
|
||||
cleaned = System.Text.RegularExpressions.Regex.Replace(cleaned, @"\s+", " ");
|
||||
|
||||
// Приводим к нижнему регистру и убираем лишние пробелы
|
||||
return cleaned.Trim().ToLower();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,5 +28,10 @@ namespace AIImages.Services
|
||||
/// Получает список доступных сэмплеров
|
||||
/// </summary>
|
||||
Task<List<string>> GetAvailableSamplers(string apiEndpoint);
|
||||
|
||||
/// <summary>
|
||||
/// Получает список доступных schedulers
|
||||
/// </summary>
|
||||
Task<List<string>> GetAvailableSchedulers(string apiEndpoint);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ namespace AIImages.Services
|
||||
BodyType = pawn.story.bodyType?.defName,
|
||||
SkinColor = pawn.story.SkinColor,
|
||||
HairStyle = pawn.story.hairDef?.label,
|
||||
HairDefName = pawn.story.hairDef?.defName,
|
||||
HairColor = pawn.story.HairColor,
|
||||
};
|
||||
|
||||
@@ -42,7 +43,9 @@ namespace AIImages.Services
|
||||
var apparelData = new ApparelData
|
||||
{
|
||||
Label = apparel.def.label,
|
||||
DefName = apparel.def.defName,
|
||||
Material = apparel.Stuff?.label,
|
||||
MaterialDefName = apparel.Stuff?.defName,
|
||||
Color = apparel.DrawColor,
|
||||
LayerType = apparel.def.apparel?.LastLayer.ToString(),
|
||||
Durability = apparel.HitPoints,
|
||||
|
||||
@@ -50,6 +50,7 @@ namespace AIImages.Services
|
||||
width = request.Width,
|
||||
height = request.Height,
|
||||
sampler_name = request.Sampler,
|
||||
scheduler = request.Scheduler,
|
||||
seed = request.Seed,
|
||||
save_images = false,
|
||||
send_images = true,
|
||||
@@ -186,6 +187,38 @@ namespace AIImages.Services
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<string>> GetAvailableSchedulers(string apiEndpoint)
|
||||
{
|
||||
try
|
||||
{
|
||||
string endpoint = $"{apiEndpoint}/sdapi/v1/schedulers";
|
||||
HttpResponseMessage response = await httpClient.GetAsync(endpoint);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return GetDefaultSchedulers();
|
||||
|
||||
string jsonResponse = await response.Content.ReadAsStringAsync();
|
||||
var schedulers = JsonConvert.DeserializeObject<List<SdScheduler>>(jsonResponse);
|
||||
|
||||
var schedulerNames = new List<string>();
|
||||
if (schedulers != null)
|
||||
{
|
||||
foreach (var scheduler in schedulers)
|
||||
{
|
||||
schedulerNames.Add(scheduler.name);
|
||||
}
|
||||
}
|
||||
|
||||
Log.Message($"[AI Images] Found {schedulerNames.Count} schedulers");
|
||||
return schedulerNames;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning($"[AI Images] Failed to load schedulers: {ex.Message}");
|
||||
return GetDefaultSchedulers();
|
||||
}
|
||||
}
|
||||
|
||||
private List<string> GetDefaultSamplers()
|
||||
{
|
||||
return new List<string>
|
||||
@@ -212,6 +245,19 @@ namespace AIImages.Services
|
||||
};
|
||||
}
|
||||
|
||||
private List<string> GetDefaultSchedulers()
|
||||
{
|
||||
return new List<string>
|
||||
{
|
||||
"Automatic",
|
||||
"Uniform",
|
||||
"Karras",
|
||||
"Exponential",
|
||||
"Polyexponential",
|
||||
"SGM Uniform",
|
||||
};
|
||||
}
|
||||
|
||||
// Вспомогательные классы для десериализации JSON ответов
|
||||
#pragma warning disable S3459, S1144 // Properties set by JSON deserializer
|
||||
private sealed class Txt2ImgResponse
|
||||
@@ -229,6 +275,11 @@ namespace AIImages.Services
|
||||
{
|
||||
public string name { get; set; }
|
||||
}
|
||||
|
||||
private sealed class SdScheduler
|
||||
{
|
||||
public string name { get; set; }
|
||||
}
|
||||
#pragma warning restore S3459, S1144
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using AIImages.Models;
|
||||
using Verse;
|
||||
|
||||
@@ -13,6 +14,17 @@ namespace AIImages.Settings
|
||||
public string apiEndpoint = "http://127.0.0.1:7860";
|
||||
public string selectedModel = "";
|
||||
public string selectedSampler = "Euler a";
|
||||
public string selectedScheduler = "Automatic";
|
||||
|
||||
// Кэшированные списки из API (не сохраняются)
|
||||
[Unsaved]
|
||||
public List<string> availableModels = new List<string>();
|
||||
|
||||
[Unsaved]
|
||||
public List<string> availableSamplers = new List<string>();
|
||||
|
||||
[Unsaved]
|
||||
public List<string> availableSchedulers = new List<string>();
|
||||
|
||||
// Настройки генерации
|
||||
public int steps = 30;
|
||||
@@ -28,7 +40,6 @@ namespace AIImages.Settings
|
||||
|
||||
// Художественный стиль
|
||||
public ArtStyle artStyle = ArtStyle.Realistic;
|
||||
public ShotType shotType = ShotType.Portrait;
|
||||
|
||||
// Путь для сохранения
|
||||
public string savePath = "AIImages/Generated";
|
||||
@@ -43,6 +54,7 @@ namespace AIImages.Settings
|
||||
Scribe_Values.Look(ref apiEndpoint, "apiEndpoint", "http://127.0.0.1:7860");
|
||||
Scribe_Values.Look(ref selectedModel, "selectedModel", "");
|
||||
Scribe_Values.Look(ref selectedSampler, "selectedSampler", "Euler a");
|
||||
Scribe_Values.Look(ref selectedScheduler, "selectedScheduler", "Automatic");
|
||||
|
||||
Scribe_Values.Look(ref steps, "steps", 30);
|
||||
Scribe_Values.Look(ref cfgScale, "cfgScale", 7.5f);
|
||||
@@ -58,7 +70,6 @@ namespace AIImages.Settings
|
||||
);
|
||||
|
||||
Scribe_Values.Look(ref artStyle, "artStyle", ArtStyle.Realistic);
|
||||
Scribe_Values.Look(ref shotType, "shotType", ShotType.Portrait);
|
||||
|
||||
Scribe_Values.Look(ref savePath, "savePath", "AIImages/Generated");
|
||||
|
||||
@@ -81,10 +92,10 @@ namespace AIImages.Settings
|
||||
Width = width,
|
||||
Height = height,
|
||||
Sampler = selectedSampler,
|
||||
Scheduler = selectedScheduler,
|
||||
Seed = seed,
|
||||
Model = selectedModel,
|
||||
ArtStyle = artStyle,
|
||||
ShotType = shotType,
|
||||
PositivePrompt = basePositivePrompt,
|
||||
NegativePrompt = baseNegativePrompt,
|
||||
};
|
||||
|
||||
@@ -21,13 +21,7 @@ namespace AIImages
|
||||
|
||||
public static void DoSettingsWindowContents(Rect inRect, AIImagesModSettings settings)
|
||||
{
|
||||
// Инициализируем буферы при первом вызове
|
||||
if (string.IsNullOrEmpty(stepsBuffer))
|
||||
{
|
||||
stepsBuffer = settings.steps.ToString();
|
||||
widthBuffer = settings.width.ToString();
|
||||
heightBuffer = settings.height.ToString();
|
||||
}
|
||||
InitializeBuffers(settings);
|
||||
|
||||
Listing_Standard listingStandard = new Listing_Standard();
|
||||
Rect viewRect = new Rect(0f, 0f, inRect.width - 20f, 1200f);
|
||||
@@ -35,7 +29,31 @@ namespace AIImages
|
||||
Widgets.BeginScrollView(inRect, ref scrollPosition, viewRect);
|
||||
listingStandard.Begin(viewRect);
|
||||
|
||||
// === API Settings ===
|
||||
DrawApiSettings(listingStandard, settings);
|
||||
DrawGenerationSettings(listingStandard, settings);
|
||||
DrawSamplerSchedulerSettings(listingStandard, settings);
|
||||
DrawPromptsSettings(listingStandard, settings);
|
||||
DrawOptionsSettings(listingStandard, settings);
|
||||
|
||||
listingStandard.End();
|
||||
Widgets.EndScrollView();
|
||||
}
|
||||
|
||||
private static void InitializeBuffers(AIImagesModSettings settings)
|
||||
{
|
||||
if (string.IsNullOrEmpty(stepsBuffer))
|
||||
{
|
||||
stepsBuffer = settings.steps.ToString();
|
||||
widthBuffer = settings.width.ToString();
|
||||
heightBuffer = settings.height.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawApiSettings(
|
||||
Listing_Standard listingStandard,
|
||||
AIImagesModSettings settings
|
||||
)
|
||||
{
|
||||
listingStandard.Label(
|
||||
"AIImages.Settings.ApiSection".Translate(),
|
||||
-1f,
|
||||
@@ -47,21 +65,66 @@ namespace AIImages
|
||||
settings.apiEndpoint = listingStandard.TextEntry(settings.apiEndpoint);
|
||||
listingStandard.Gap(8f);
|
||||
|
||||
// Кнопка проверки подключения
|
||||
if (listingStandard.ButtonText("AIImages.Settings.TestConnection".Translate()))
|
||||
{
|
||||
_ = TestApiConnection(settings.apiEndpoint);
|
||||
}
|
||||
|
||||
// Кнопка загрузки моделей
|
||||
if (listingStandard.ButtonText("AIImages.Settings.LoadModels".Translate()))
|
||||
{
|
||||
_ = LoadModelsFromApi(settings);
|
||||
}
|
||||
|
||||
listingStandard.Gap(12f);
|
||||
DrawModelDropdown(listingStandard, settings);
|
||||
|
||||
// === Generation Settings ===
|
||||
if (listingStandard.ButtonText("AIImages.Settings.LoadSamplersSchedulers".Translate()))
|
||||
{
|
||||
_ = LoadSamplersAndSchedulers(settings);
|
||||
}
|
||||
|
||||
listingStandard.Gap(12f);
|
||||
}
|
||||
|
||||
private static void DrawModelDropdown(
|
||||
Listing_Standard listingStandard,
|
||||
AIImagesModSettings settings
|
||||
)
|
||||
{
|
||||
if (
|
||||
listingStandard.ButtonTextLabeled(
|
||||
"AIImages.Settings.Model".Translate(),
|
||||
string.IsNullOrEmpty(settings.selectedModel)
|
||||
? "AIImages.Settings.NoModelSelected".Translate()
|
||||
: settings.selectedModel
|
||||
)
|
||||
)
|
||||
{
|
||||
List<FloatMenuOption> modelOptions = new List<FloatMenuOption>();
|
||||
if (!settings.availableModels.Any())
|
||||
{
|
||||
modelOptions.Add(
|
||||
new FloatMenuOption("AIImages.Settings.LoadModelsFirst".Translate(), null)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (string model in settings.availableModels)
|
||||
{
|
||||
string localModel = model;
|
||||
modelOptions.Add(
|
||||
new FloatMenuOption(model, () => settings.selectedModel = localModel)
|
||||
);
|
||||
}
|
||||
}
|
||||
Find.WindowStack.Add(new FloatMenu(modelOptions));
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawGenerationSettings(
|
||||
Listing_Standard listingStandard,
|
||||
AIImagesModSettings settings
|
||||
)
|
||||
{
|
||||
listingStandard.Label(
|
||||
"AIImages.Settings.GenerationSection".Translate(),
|
||||
-1f,
|
||||
@@ -69,7 +132,28 @@ namespace AIImages
|
||||
);
|
||||
listingStandard.GapLine();
|
||||
|
||||
// Art Style
|
||||
DrawArtStyleDropdown(listingStandard, settings);
|
||||
listingStandard.Gap(8f);
|
||||
|
||||
listingStandard.Label("AIImages.Settings.Steps".Translate() + $": {settings.steps}");
|
||||
settings.steps = (int)listingStandard.Slider(settings.steps, 1, 150);
|
||||
listingStandard.Gap(8f);
|
||||
|
||||
listingStandard.Label(
|
||||
"AIImages.Settings.CfgScale".Translate() + $": {settings.cfgScale:F1}"
|
||||
);
|
||||
settings.cfgScale = listingStandard.Slider(settings.cfgScale, 1f, 30f);
|
||||
listingStandard.Gap(8f);
|
||||
|
||||
DrawSizeSettings(listingStandard, settings);
|
||||
listingStandard.Gap(12f);
|
||||
}
|
||||
|
||||
private static void DrawArtStyleDropdown(
|
||||
Listing_Standard listingStandard,
|
||||
AIImagesModSettings settings
|
||||
)
|
||||
{
|
||||
if (
|
||||
listingStandard.ButtonTextLabeled(
|
||||
"AIImages.Settings.ArtStyle".Translate(),
|
||||
@@ -87,41 +171,13 @@ namespace AIImages
|
||||
}
|
||||
Find.WindowStack.Add(new FloatMenu(styleOptions));
|
||||
}
|
||||
}
|
||||
|
||||
// Shot Type
|
||||
if (
|
||||
listingStandard.ButtonTextLabeled(
|
||||
"AIImages.Settings.ShotType".Translate(),
|
||||
settings.shotType.ToString()
|
||||
)
|
||||
)
|
||||
{
|
||||
List<FloatMenuOption> shotOptions = new List<FloatMenuOption>();
|
||||
foreach (ShotType shot in Enum.GetValues(typeof(ShotType)))
|
||||
{
|
||||
ShotType localShot = shot;
|
||||
shotOptions.Add(
|
||||
new FloatMenuOption(shot.ToString(), () => settings.shotType = localShot)
|
||||
);
|
||||
}
|
||||
Find.WindowStack.Add(new FloatMenu(shotOptions));
|
||||
}
|
||||
|
||||
listingStandard.Gap(8f);
|
||||
|
||||
// Steps
|
||||
listingStandard.Label("AIImages.Settings.Steps".Translate() + $": {settings.steps}");
|
||||
settings.steps = (int)listingStandard.Slider(settings.steps, 1, 150);
|
||||
listingStandard.Gap(8f);
|
||||
|
||||
// CFG Scale
|
||||
listingStandard.Label(
|
||||
"AIImages.Settings.CfgScale".Translate() + $": {settings.cfgScale:F1}"
|
||||
);
|
||||
settings.cfgScale = listingStandard.Slider(settings.cfgScale, 1f, 30f);
|
||||
listingStandard.Gap(8f);
|
||||
|
||||
// Width
|
||||
private static void DrawSizeSettings(
|
||||
Listing_Standard listingStandard,
|
||||
AIImagesModSettings settings
|
||||
)
|
||||
{
|
||||
listingStandard.Label("AIImages.Settings.Width".Translate() + ":");
|
||||
widthBuffer = listingStandard.TextEntry(widthBuffer);
|
||||
if (int.TryParse(widthBuffer, out int width))
|
||||
@@ -129,7 +185,6 @@ namespace AIImages
|
||||
settings.width = Mathf.Clamp(width, 64, 2048);
|
||||
}
|
||||
|
||||
// Height
|
||||
listingStandard.Label("AIImages.Settings.Height".Translate() + ":");
|
||||
heightBuffer = listingStandard.TextEntry(heightBuffer);
|
||||
if (int.TryParse(heightBuffer, out int height))
|
||||
@@ -137,41 +192,119 @@ namespace AIImages
|
||||
settings.height = Mathf.Clamp(height, 64, 2048);
|
||||
}
|
||||
|
||||
// Common size presets
|
||||
DrawSizePresets(listingStandard, settings);
|
||||
}
|
||||
|
||||
private static void DrawSizePresets(
|
||||
Listing_Standard listingStandard,
|
||||
AIImagesModSettings settings
|
||||
)
|
||||
{
|
||||
listingStandard.Gap(4f);
|
||||
Rect presetRect = listingStandard.GetRect(30f);
|
||||
if (Widgets.ButtonText(new Rect(presetRect.x, presetRect.y, 80f, 30f), "512x512"))
|
||||
Rect presetRect1 = listingStandard.GetRect(30f);
|
||||
DrawPresetButton(presetRect1, 0f, "512x512", 512, 512, settings);
|
||||
DrawPresetButton(presetRect1, 85f, "512x768", 512, 768, settings);
|
||||
DrawPresetButton(presetRect1, 170f, "768x768", 768, 768, settings);
|
||||
|
||||
listingStandard.Gap(4f);
|
||||
Rect presetRect2 = listingStandard.GetRect(30f);
|
||||
DrawPresetButton(presetRect2, 0f, "896x1152", 896, 1152, settings, 90f);
|
||||
DrawPresetButton(presetRect2, 95f, "1024x1024", 1024, 1024, settings, 90f);
|
||||
}
|
||||
|
||||
private static void DrawPresetButton(
|
||||
Rect rect,
|
||||
float xOffset,
|
||||
string label,
|
||||
int width,
|
||||
int height,
|
||||
AIImagesModSettings settings,
|
||||
float buttonWidth = 80f
|
||||
)
|
||||
{
|
||||
if (Widgets.ButtonText(new Rect(rect.x + xOffset, rect.y, buttonWidth, 30f), label))
|
||||
{
|
||||
settings.width = 512;
|
||||
settings.height = 512;
|
||||
widthBuffer = "512";
|
||||
heightBuffer = "512";
|
||||
}
|
||||
if (Widgets.ButtonText(new Rect(presetRect.x + 85f, presetRect.y, 80f, 30f), "512x768"))
|
||||
{
|
||||
settings.width = 512;
|
||||
settings.height = 768;
|
||||
widthBuffer = "512";
|
||||
heightBuffer = "768";
|
||||
settings.width = width;
|
||||
settings.height = height;
|
||||
widthBuffer = width.ToString();
|
||||
heightBuffer = height.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawSamplerSchedulerSettings(
|
||||
Listing_Standard listingStandard,
|
||||
AIImagesModSettings settings
|
||||
)
|
||||
{
|
||||
DrawSamplerDropdown(listingStandard, settings);
|
||||
DrawSchedulerDropdown(listingStandard, settings);
|
||||
listingStandard.Gap(12f);
|
||||
}
|
||||
|
||||
private static void DrawSamplerDropdown(
|
||||
Listing_Standard listingStandard,
|
||||
AIImagesModSettings settings
|
||||
)
|
||||
{
|
||||
if (
|
||||
Widgets.ButtonText(new Rect(presetRect.x + 170f, presetRect.y, 80f, 30f), "768x768")
|
||||
listingStandard.ButtonTextLabeled(
|
||||
"AIImages.Settings.Sampler".Translate(),
|
||||
settings.selectedSampler
|
||||
)
|
||||
)
|
||||
{
|
||||
settings.width = 768;
|
||||
settings.height = 768;
|
||||
widthBuffer = "768";
|
||||
heightBuffer = "768";
|
||||
List<FloatMenuOption> samplerOptions = new List<FloatMenuOption>();
|
||||
var availableSamplers = settings.availableSamplers.Any()
|
||||
? settings.availableSamplers
|
||||
: new List<string> { settings.selectedSampler };
|
||||
|
||||
foreach (string sampler in availableSamplers)
|
||||
{
|
||||
string localSampler = sampler;
|
||||
samplerOptions.Add(
|
||||
new FloatMenuOption(sampler, () => settings.selectedSampler = localSampler)
|
||||
);
|
||||
}
|
||||
Find.WindowStack.Add(new FloatMenu(samplerOptions));
|
||||
}
|
||||
}
|
||||
|
||||
listingStandard.Gap(12f);
|
||||
private static void DrawSchedulerDropdown(
|
||||
Listing_Standard listingStandard,
|
||||
AIImagesModSettings settings
|
||||
)
|
||||
{
|
||||
if (
|
||||
listingStandard.ButtonTextLabeled(
|
||||
"AIImages.Settings.Scheduler".Translate(),
|
||||
settings.selectedScheduler
|
||||
)
|
||||
)
|
||||
{
|
||||
List<FloatMenuOption> schedulerOptions = new List<FloatMenuOption>();
|
||||
var availableSchedulers = settings.availableSchedulers.Any()
|
||||
? settings.availableSchedulers
|
||||
: new List<string> { settings.selectedScheduler };
|
||||
|
||||
// Sampler
|
||||
listingStandard.Label("AIImages.Settings.Sampler".Translate() + ":");
|
||||
settings.selectedSampler = listingStandard.TextEntry(settings.selectedSampler);
|
||||
listingStandard.Gap(12f);
|
||||
foreach (string scheduler in availableSchedulers)
|
||||
{
|
||||
string localScheduler = scheduler;
|
||||
schedulerOptions.Add(
|
||||
new FloatMenuOption(
|
||||
scheduler,
|
||||
() => settings.selectedScheduler = localScheduler
|
||||
)
|
||||
);
|
||||
}
|
||||
Find.WindowStack.Add(new FloatMenu(schedulerOptions));
|
||||
}
|
||||
}
|
||||
|
||||
// === Prompts ===
|
||||
private static void DrawPromptsSettings(
|
||||
Listing_Standard listingStandard,
|
||||
AIImagesModSettings settings
|
||||
)
|
||||
{
|
||||
listingStandard.Label(
|
||||
"AIImages.Settings.PromptsSection".Translate(),
|
||||
-1f,
|
||||
@@ -186,8 +319,13 @@ namespace AIImages
|
||||
listingStandard.Label("AIImages.Settings.BaseNegativePrompt".Translate() + ":");
|
||||
settings.baseNegativePrompt = listingStandard.TextEntry(settings.baseNegativePrompt, 3);
|
||||
listingStandard.Gap(12f);
|
||||
}
|
||||
|
||||
// === Options ===
|
||||
private static void DrawOptionsSettings(
|
||||
Listing_Standard listingStandard,
|
||||
AIImagesModSettings settings
|
||||
)
|
||||
{
|
||||
listingStandard.Label("AIImages.Settings.OptionsSection".Translate());
|
||||
listingStandard.GapLine();
|
||||
|
||||
@@ -206,12 +344,8 @@ namespace AIImages
|
||||
|
||||
listingStandard.Gap(12f);
|
||||
|
||||
// Save path
|
||||
listingStandard.Label("AIImages.Settings.SavePath".Translate() + ":");
|
||||
settings.savePath = listingStandard.TextEntry(settings.savePath);
|
||||
|
||||
listingStandard.End();
|
||||
Widgets.EndScrollView();
|
||||
}
|
||||
|
||||
private static async System.Threading.Tasks.Task TestApiConnection(string endpoint)
|
||||
@@ -250,6 +384,7 @@ namespace AIImages
|
||||
{
|
||||
Log.Message("[AI Images] Loading models from API...");
|
||||
var models = await AIImagesMod.ApiService.GetAvailableModels(settings.apiEndpoint);
|
||||
settings.availableModels = models;
|
||||
|
||||
if (models.Count > 0)
|
||||
{
|
||||
@@ -258,8 +393,13 @@ namespace AIImages
|
||||
MessageTypeDefOf.PositiveEvent
|
||||
);
|
||||
|
||||
// Если модель не выбрана, выбираем первую
|
||||
if (string.IsNullOrEmpty(settings.selectedModel) && models.Count > 0)
|
||||
if (
|
||||
(
|
||||
string.IsNullOrEmpty(settings.selectedModel)
|
||||
|| !models.Contains(settings.selectedModel)
|
||||
)
|
||||
&& models.Count > 0
|
||||
)
|
||||
{
|
||||
settings.selectedModel = models[0];
|
||||
}
|
||||
@@ -280,5 +420,73 @@ namespace AIImages
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static async System.Threading.Tasks.Task LoadSamplersAndSchedulers(
|
||||
AIImagesModSettings settings
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
Log.Message("[AI Images] Loading samplers and schedulers from API...");
|
||||
|
||||
var samplers = await AIImagesMod.ApiService.GetAvailableSamplers(
|
||||
settings.apiEndpoint
|
||||
);
|
||||
settings.availableSamplers = samplers;
|
||||
|
||||
var schedulers = await AIImagesMod.ApiService.GetAvailableSchedulers(
|
||||
settings.apiEndpoint
|
||||
);
|
||||
settings.availableSchedulers = schedulers;
|
||||
|
||||
int totalCount = samplers.Count + schedulers.Count;
|
||||
if (totalCount > 0)
|
||||
{
|
||||
Messages.Message(
|
||||
"AIImages.Settings.SamplersSchedulersLoaded".Translate(
|
||||
samplers.Count,
|
||||
schedulers.Count
|
||||
),
|
||||
MessageTypeDefOf.PositiveEvent
|
||||
);
|
||||
|
||||
if (
|
||||
(
|
||||
string.IsNullOrEmpty(settings.selectedSampler)
|
||||
|| !samplers.Contains(settings.selectedSampler)
|
||||
)
|
||||
&& samplers.Count > 0
|
||||
)
|
||||
{
|
||||
settings.selectedSampler = samplers[0];
|
||||
}
|
||||
|
||||
if (
|
||||
(
|
||||
string.IsNullOrEmpty(settings.selectedScheduler)
|
||||
|| !schedulers.Contains(settings.selectedScheduler)
|
||||
)
|
||||
&& schedulers.Count > 0
|
||||
)
|
||||
{
|
||||
settings.selectedScheduler = schedulers[0];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Messages.Message(
|
||||
"AIImages.Settings.NoSamplersSchedulersFound".Translate(),
|
||||
MessageTypeDefOf.RejectInput
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Messages.Message(
|
||||
$"Error loading samplers/schedulers: {ex.Message}",
|
||||
MessageTypeDefOf.RejectInput
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@ namespace AIImages
|
||||
Width = generationSettings.Width,
|
||||
Height = generationSettings.Height,
|
||||
Sampler = generationSettings.Sampler,
|
||||
Scheduler = generationSettings.Scheduler,
|
||||
Seed = generationSettings.Seed,
|
||||
Model = AIImagesMod.Settings.apiEndpoint,
|
||||
};
|
||||
@@ -269,8 +270,8 @@ namespace AIImages
|
||||
{
|
||||
float imageSize = Mathf.Min(rect.width, 400f);
|
||||
Rect imageRect = new Rect(
|
||||
(rect.width - imageSize) / 2f,
|
||||
curY,
|
||||
rect.x + (rect.width - imageSize) / 2f,
|
||||
rect.y + curY,
|
||||
imageSize,
|
||||
imageSize
|
||||
);
|
||||
@@ -282,8 +283,8 @@ namespace AIImages
|
||||
// Placeholder для изображения
|
||||
float placeholderSize = Mathf.Min(rect.width, 300f);
|
||||
Rect placeholderRect = new Rect(
|
||||
(rect.width - placeholderSize) / 2f,
|
||||
curY,
|
||||
rect.x + (rect.width - placeholderSize) / 2f,
|
||||
rect.y + curY,
|
||||
placeholderSize,
|
||||
placeholderSize
|
||||
);
|
||||
@@ -299,7 +300,10 @@ namespace AIImages
|
||||
{
|
||||
Text.Font = GameFont.Small;
|
||||
float statusHeight = Text.CalcHeight(generationStatus, rect.width);
|
||||
Widgets.Label(new Rect(0f, curY, rect.width, statusHeight), generationStatus);
|
||||
Widgets.Label(
|
||||
new Rect(rect.x, rect.y + curY, rect.width, statusHeight),
|
||||
generationStatus
|
||||
);
|
||||
curY += statusHeight + 10f;
|
||||
}
|
||||
|
||||
@@ -307,7 +311,7 @@ namespace AIImages
|
||||
Text.Font = GameFont.Small;
|
||||
if (
|
||||
Widgets.ButtonText(
|
||||
new Rect(0f, curY, rect.width, 35f),
|
||||
new Rect(rect.x, rect.y + curY, rect.width, 35f),
|
||||
isGenerating
|
||||
? "AIImages.Generation.Generating".Translate()
|
||||
: "AIImages.Generation.Generate".Translate()
|
||||
@@ -321,7 +325,7 @@ namespace AIImages
|
||||
// Промпт секция
|
||||
Text.Font = GameFont.Medium;
|
||||
Widgets.Label(
|
||||
new Rect(0f, curY, rect.width, 30f),
|
||||
new Rect(rect.x, rect.y + curY, rect.width, 30f),
|
||||
"AIImages.Prompt.SectionTitle".Translate()
|
||||
);
|
||||
curY += 35f;
|
||||
@@ -334,7 +338,7 @@ namespace AIImages
|
||||
);
|
||||
|
||||
float promptHeight = Mathf.Min(Text.CalcHeight(promptText, rect.width), 150f);
|
||||
Rect promptRect = new Rect(0f, curY, rect.width, promptHeight);
|
||||
Rect promptRect = new Rect(rect.x, rect.y + curY, rect.width, promptHeight);
|
||||
|
||||
// Рисуем промпт в скроллируемой области если он длинный
|
||||
Widgets.DrawBoxSolid(promptRect, new Color(0.1f, 0.1f, 0.1f, 0.5f));
|
||||
@@ -344,7 +348,7 @@ namespace AIImages
|
||||
// Кнопка копирования промпта
|
||||
if (
|
||||
Widgets.ButtonText(
|
||||
new Rect(0f, curY, rect.width / 2f - 5f, 30f),
|
||||
new Rect(rect.x, rect.y + curY, rect.width / 2f - 5f, 30f),
|
||||
"AIImages.Prompt.CopyButton".Translate()
|
||||
)
|
||||
)
|
||||
@@ -356,7 +360,12 @@ namespace AIImages
|
||||
// Кнопка обновления данных
|
||||
if (
|
||||
Widgets.ButtonText(
|
||||
new Rect(rect.width / 2f + 5f, curY, rect.width / 2f - 5f, 30f),
|
||||
new Rect(
|
||||
rect.x + rect.width / 2f + 5f,
|
||||
rect.y + curY,
|
||||
rect.width / 2f - 5f,
|
||||
30f
|
||||
),
|
||||
"AIImages.Window.Refresh".Translate()
|
||||
)
|
||||
)
|
||||
@@ -370,7 +379,7 @@ namespace AIImages
|
||||
curY += 35f;
|
||||
GUI.color = new Color(0f, 1f, 0f, copiedMessageTime / 2f);
|
||||
Widgets.Label(
|
||||
new Rect(0f, curY, rect.width, 25f),
|
||||
new Rect(rect.x, rect.y + curY, rect.width, 25f),
|
||||
"AIImages.Prompt.Copied".Translate()
|
||||
);
|
||||
GUI.color = Color.white;
|
||||
|
||||
Reference in New Issue
Block a user