Compare commits

..

31 Commits

Author SHA1 Message Date
Leonid Pershin
59abcb11b8 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
2025-11-01 14:27:14 +03:00
Leonid Pershin
5c9887c669 Update PawnGizmoPatch to add a gizmo button to all spawned pawns instead of just colonists. Modify Window_AIImage to handle selection of any spawned pawn, enhancing functionality and usability across the AIImages mod.
All checks were successful
SonarQube Analysis / Build and analyze (push) Successful in 1m38s
2025-11-01 09:27:32 +03:00
Leonid Pershin
a9dd7910a0 Refactor ColorDescriptionService to improve skin and hair color handling by introducing new methods for determining colors based on gene definitions. Add constants for color keywords to enhance readability and maintainability. Update AIImages.dll to reflect these changes.
All checks were successful
SonarQube Analysis / Build and analyze (push) Successful in 1m51s
2025-11-01 09:18:36 +03:00
Leonid Pershin
30010078b4 Enhance hair color handling in AIImages by adding support for hair color genes in PawnAppearanceData. Update ColorDescriptionService to prioritize gene-based hair color descriptions, improving character appearance representation. Refactor related services to utilize new hair color logic.
All checks were successful
SonarQube Analysis / Build and analyze (push) Successful in 1m40s
2025-11-01 09:15:29 +03:00
Leonid Pershin
60dcb279ae Add skin color gene handling to PawnAppearanceData and update skin tone description logic. Enhance AIImages.dll to support special skin tones based on gene definitions, improving character appearance representation.
All checks were successful
SonarQube Analysis / Build and analyze (push) Successful in 1m57s
2025-11-01 08:51:49 +03:00
Leonid Pershin
117f27effc Remove Quality Gate wait step from SonarQube workflow to simplify the analysis process and improve efficiency.
All checks were successful
SonarQube Analysis / Build and analyze (push) Successful in 1m54s
2025-10-31 21:06:40 +03:00
Leonid Pershin
8721b7bc61 Remove redundant source encoding parameter in SonarQube workflow to streamline configuration.
Some checks failed
SonarQube Analysis / Build and analyze (push) Failing after 2m2s
2025-10-31 18:56:36 +03:00
Leonid Pershin
714969cfb9 Refactor SonarQube workflow to streamline commands by directly referencing the solution file for restore, build, and test steps, enhancing clarity and efficiency.
Some checks failed
SonarQube Analysis / Build and analyze (push) Failing after 1m26s
2025-10-31 18:43:04 +03:00
Leonid Pershin
5f5f6215b1 Fix typo in SonarQube workflow name from "SonarQube Analys" to "SonarQube Analysis".
Some checks failed
SonarQube Analys / Build and analyze (push) Failing after 1m37s
2025-10-31 18:39:00 +03:00
Leonid Pershin
5d493b6ed5 Rename SonarQube workflow to "SonarQube Analysis" for clarity.
Some checks failed
SonarQube Analysis / Build and analyze (push) Failing after 1m38s
2025-10-31 18:34:56 +03:00
Leonid Pershin
90f6348e4d Update SonarQube workflow to trigger on push events for all branches and modify project key references in API calls for consistency.
Some checks failed
SonarQube / Build and analyze (push) Failing after 3m1s
2025-10-31 18:28:46 +03:00
Leonid Pershin
bc361cd011 Enhance AIImages mod by adding a feature to manage image favorites, allowing users to mark images as favorites for easier access. Update UI components to include a favorites button and modify image management functionality accordingly. Add localization strings for the favorites feature in English and Russian. Update AIImages.dll to reflect these changes. 2025-10-31 18:26:33 +03:00
Leonid Pershin
a99fa16763 Implement AI Images gallery feature in RimWorld mod, allowing users to view, delete, and manage generated images. Update UI components to include a gallery button and enhance image management functionality. Add localization strings for gallery features in English and Russian. Update AIImages.dll to reflect these changes. 2025-10-31 18:20:39 +03:00
Leonid Pershin
1b35cb6a44 Add image type selection feature in AIImages mod, allowing users to choose between portrait and full body images. Update English and Russian localization files to include new labels for image types. Modify UI components to render the image type selector and adjust generation settings accordingly. Update AIImages.dll to reflect these changes. 2025-10-31 10:46:53 +03:00
Leonid Pershin
731428fb44 Refactor character information display in AIImages mod by modularizing the rendering of basic info, traits, genes, hediffs, and apparel into separate methods. Enhance height calculation logic for improved layout consistency. Update AIImages.dll to reflect these changes. 2025-10-31 10:13:42 +03:00
Leonid Pershin
1ec80a01cb Refactor skin tone description logic in AIImages mod to prioritize gene-based descriptions over color fallback. Update ColorDescriptionService to include new methods for gene skin tone extraction. Modify related services and UI components to utilize the updated skin tone logic. Update AIImages.dll to reflect these changes. 2025-10-31 10:07:39 +03:00
Leonid Pershin
51c3ea4bc1 Add Genes and Hediffs information display in AIImages mod. Update English and Russian localization files to include new labels. Modify Window_AIImage.cs to render gene and hediff details in the character information display. Update AIImages.dll to reflect these changes. 2025-10-28 19:53:04 +03:00
Leonid Pershin
5f8f29a7dc Enhance AIImages mod by adding debug logging for image clearing and window redraw events, improving visibility into state changes. Update AIImages.dll to reflect these changes. 2025-10-28 19:22:22 +03:00
Leonid Pershin
beb1e2b2fc Enhance AIImages mod by adding debug logging functionality to various components, improving troubleshooting capabilities. Introduce a new setting to enable or disable debug logs in the UI, with localized strings in English and Russian. Update StableDiffusionNet.Core dependency to version 1.1.5. Update AIImages.dll to reflect these changes. 2025-10-28 19:01:38 +03:00
Leonid Pershin
0bdcd3036a Refactor AIImages mod to improve UI layout and scrolling functionality in the character information display. Update prompt generation logic to enhance gender and age representation. Adjust content height calculations for better visual consistency. Update AIImages.dll to reflect these changes. 2025-10-27 02:00:07 +03:00
Leonid Pershin
e3a90d6186 Update AIImages mod to improve image size presets by removing redundant labels and enhancing character information display. Add collapsible sections for positive and negative prompts in the UI, along with localized strings in English and Russian. Update AIImages.dll to reflect these changes. 2025-10-27 00:45:55 +03:00
Leonid Pershin
ce98638e55 Refactor AIImages mod to replace ArtStyle enum with ArtStyleDef name for improved flexibility in prompt generation. Update UI components to reflect changes in art style selection and enhance scrolling functionality in the right column. Update AIImages.dll to incorporate the latest modifications. 2025-10-27 00:19:31 +03:00
Leonid Pershin
9e675dd804 Add CurrentImage property to GenerationRequest model for preview functionality and update StableDiffusionNetAdapter to include current image in progress tracking. Update AIImages.dll to reflect these changes. 2025-10-27 00:08:06 +03:00
Leonid Pershin
379865a6aa Implement "Save Images to Server" feature in AIImages mod, allowing users to save generated images to the Stable Diffusion server. Update UI with new settings option and localized strings in English and Russian. Upgrade StableDiffusionNet.Core dependency to version 1.1.2. Update AIImages.dll to reflect these changes. 2025-10-27 00:03:26 +03:00
Leonid Pershin
9fb05e4e7e Add functionality to clear all generated portrait images in AIImages mod. Implement UI button for clearing images and add confirmation dialog. Update localized strings in English and Russian for new feature. Update AIImages.dll to reflect these changes. 2025-10-26 23:16:08 +03:00
Leonid Pershin
ff5f679c4a Enhance AIImages mod by adding support for positive and negative prompts, including UI updates for displaying and copying these prompts. Localized strings have been updated in English and Russian to reflect these changes. Update AIImages.dll to incorporate the latest modifications. 2025-10-26 23:03:09 +03:00
Leonid Pershin
d67ec8c0ac Implement progress monitoring for image generation in AIImages mod, enhancing user experience with real-time updates. Add localized strings for new features in English and Russian. Refactor UI components for better organization and clarity. Update AIImages.dll to reflect these changes. 2025-10-26 22:56:38 +03:00
Leonid Pershin
b9d7ea0c04 Remove obsolete binary assemblies from the project, including Microsoft.Extensions and Polly libraries, to streamline dependencies and reduce project size. Update AIImages.dll to reflect the latest changes. 2025-10-26 22:30:25 +03:00
Leonid Pershin
6e6e92df53 Update AIImages mod to utilize StableDiffusionNet.Core for API interactions, enhancing image generation capabilities. Refactor service methods for improved clarity and efficiency, and update AIImages.dll to reflect these changes. 2025-10-26 22:29:27 +03:00
Leonid Pershin
02b0143186 Enhance AIImages mod by adding cancellation support for image generation, improving user experience with localized strings for cancellation actions in English and Russian. Refactor service integration for better dependency management and update AIImages.dll to reflect these changes. 2025-10-26 19:10:45 +03:00
Leonid Pershin
3434927342 Refactor AIImages mod to streamline API loading functionality, consolidating model, sampler, and scheduler loading into a single method. Update localized strings in English and Russian for improved clarity and consistency. Enhance UI with scrollable prompt display for better user experience. Update AIImages.dll to reflect these changes. 2025-10-26 18:38:18 +03:00
56 changed files with 5301 additions and 806 deletions

69
.github/workflows/sonarqube.yml vendored Normal file
View File

@@ -0,0 +1,69 @@
name: SonarQube Analysis
on:
push:
branches:
- '*'
jobs:
build:
name: Build and analyze
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: 17
distribution: 'zulu' # Alternative distribution options are available.
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x' # .NET 8 SDK supports building .NET Framework 4.7.2 projects
- name: Install SonarQube Cloud scanner
run: |
mkdir -p ~/.sonar/scanner
dotnet tool install dotnet-sonarscanner --tool-path ~/.sonar/scanner
- name: Restore dependencies
run: dotnet restore AIImages.sln --verbosity normal
- name: Build and analyze
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: |
echo "Starting SonarQube analysis..."
echo "Current directory: $(pwd)"
echo "Listing files:"
ls -la
echo "Starting SonarQube scanner..."
~/.sonar/scanner/dotnet-sonarscanner begin \
/k:"AIImages" \
/d:sonar.token="${{ secrets.SONAR_TOKEN }}" \
/d:sonar.host.url="${{ secrets.SONAR_HOST_URL }}" \
/d:sonar.projectBaseDir="$(pwd)" \
/d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" \
/d:sonar.coverage.exclusions="**/obj/**,**/bin/**,**/Assemblies/**" \
/d:sonar.exclusions="**/obj/**,**/bin/**,**/Assemblies/**,**/Migrations/**" \
/d:sonar.cpd.exclusions="**/obj/**,**/bin/**" \
/d:sonar.test.inclusions="**/*Tests.cs,**/*Test.cs" \
/d:sonar.sources="Source" \
/d:sonar.sourceEncoding=UTF-8
echo "Building solution..."
dotnet build AIImages.sln --verbosity normal --no-incremental -c Release
echo "Running tests with coverage (if any)..."
dotnet test AIImages.sln /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=./coverage/ /p:Exclude="[*.Tests]*" || echo "No tests found, skipping test execution"
echo "Ending SonarQube analysis..."
~/.sonar/scanner/dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

111
Defs/ArtStyleDefs.xml Normal file
View File

@@ -0,0 +1,111 @@
<?xml version="1.0" encoding="utf-8"?>
<Defs>
<!-- None/Custom Style -->
<AIImages.ArtStyleDef>
<defName>ArtStyle_None</defName>
<label>None (Custom)</label>
<description>No predefined style - use only your custom prompts</description>
<positivePrompt></positivePrompt>
<negativePrompt></negativePrompt>
<qualityTags></qualityTags>
<addBaseQualityTags>false</addBaseQualityTags>
<addBaseNegativePrompts>false</addBaseNegativePrompts>
<sortOrder>0</sortOrder>
</AIImages.ArtStyleDef>
<!-- Realistic Style -->
<AIImages.ArtStyleDef>
<defName>ArtStyle_Realistic</defName>
<label>Realistic</label>
<description>Photorealistic style with high detail</description>
<positivePrompt>photorealistic, hyperrealistic, realistic photo, photography</positivePrompt>
<negativePrompt>cartoon, anime, painting, drawing, illustration</negativePrompt>
<qualityTags>professional photography, 8k uhd, dslr, high quality, sharp focus</qualityTags>
<addBaseQualityTags>true</addBaseQualityTags>
<addBaseNegativePrompts>true</addBaseNegativePrompts>
<sortOrder>10</sortOrder>
</AIImages.ArtStyleDef>
<!-- Semi-Realistic Style -->
<AIImages.ArtStyleDef>
<defName>ArtStyle_SemiRealistic</defName>
<label>Semi-Realistic</label>
<description>Detailed illustration with realistic elements</description>
<positivePrompt>semi-realistic, detailed illustration, realistic art</positivePrompt>
<negativePrompt>cartoon, anime, painting, drawing, illustration</negativePrompt>
<qualityTags>professional photography, 8k uhd, dslr, high quality, sharp focus</qualityTags>
<addBaseQualityTags>true</addBaseQualityTags>
<addBaseNegativePrompts>true</addBaseNegativePrompts>
<sortOrder>20</sortOrder>
</AIImages.ArtStyleDef>
<!-- Anime Style -->
<AIImages.ArtStyleDef>
<defName>ArtStyle_Anime</defName>
<label>Anime</label>
<description>Japanese anime/manga style</description>
<positivePrompt>anime style, manga style, anime character</positivePrompt>
<negativePrompt>realistic, photo, photography, 3d</negativePrompt>
<qualityTags>anime masterpiece, high resolution, vibrant colors</qualityTags>
<addBaseQualityTags>true</addBaseQualityTags>
<addBaseNegativePrompts>true</addBaseNegativePrompts>
<sortOrder>30</sortOrder>
</AIImages.ArtStyleDef>
<!-- Concept Art Style -->
<AIImages.ArtStyleDef>
<defName>ArtStyle_ConceptArt</defName>
<label>Concept Art</label>
<description>Professional digital concept art</description>
<positivePrompt>concept art, digital art, artstation, professional concept design</positivePrompt>
<negativePrompt></negativePrompt>
<qualityTags>trending on artstation, professional digital art</qualityTags>
<addBaseQualityTags>true</addBaseQualityTags>
<addBaseNegativePrompts>true</addBaseNegativePrompts>
<sortOrder>40</sortOrder>
</AIImages.ArtStyleDef>
<!-- Digital Painting Style -->
<AIImages.ArtStyleDef>
<defName>ArtStyle_DigitalPainting</defName>
<label>Digital Painting</label>
<description>Digital painting with brush strokes</description>
<positivePrompt>digital painting, painterly, brush strokes, artistic</positivePrompt>
<negativePrompt></negativePrompt>
<qualityTags></qualityTags>
<addBaseQualityTags>true</addBaseQualityTags>
<addBaseNegativePrompts>true</addBaseNegativePrompts>
<sortOrder>50</sortOrder>
</AIImages.ArtStyleDef>
<!-- Oil Painting Style -->
<AIImages.ArtStyleDef>
<defName>ArtStyle_OilPainting</defName>
<label>Oil Painting</label>
<description>Traditional oil painting style</description>
<positivePrompt>oil painting, traditional painting, canvas, fine art</positivePrompt>
<negativePrompt></negativePrompt>
<qualityTags></qualityTags>
<addBaseQualityTags>true</addBaseQualityTags>
<addBaseNegativePrompts>true</addBaseNegativePrompts>
<sortOrder>60</sortOrder>
</AIImages.ArtStyleDef>
<!-- Sketch Style -->
<AIImages.ArtStyleDef>
<defName>ArtStyle_Sketch</defName>
<label>Sketch</label>
<description>Pencil sketch or line art</description>
<positivePrompt>pencil sketch, hand drawn, sketch art, line art</positivePrompt>
<negativePrompt></negativePrompt>
<qualityTags></qualityTags>
<addBaseQualityTags>true</addBaseQualityTags>
<addBaseNegativePrompts>true</addBaseNegativePrompts>
<sortOrder>70</sortOrder>
</AIImages.ArtStyleDef>
<!-- Cell Shaded Style -->
<AIImages.ArtStyleDef>
<defName>ArtStyle_CellShaded</defName>
<label>Cell Shaded</label>
<description>Flat colors with toon shading</description>
<positivePrompt>cell shaded, flat colors, toon shading, stylized</positivePrompt>
<negativePrompt></negativePrompt>
<qualityTags></qualityTags>
<addBaseQualityTags>true</addBaseQualityTags>
<addBaseNegativePrompts>true</addBaseNegativePrompts>
<sortOrder>80</sortOrder>
</AIImages.ArtStyleDef>
</Defs>

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<Defs>
<!-- Square Sizes -->
<AIImages.ImageSizePresetDef>
<defName>Size_512x512</defName>
<label>512x512</label>
<width>512</width>
<height>512</height>
<category>Square</category>
<sortOrder>10</sortOrder>
</AIImages.ImageSizePresetDef>
<AIImages.ImageSizePresetDef>
<defName>Size_768x768</defName>
<label>768x768</label>
<width>768</width>
<height>768</height>
<category>Square</category>
<sortOrder>20</sortOrder>
</AIImages.ImageSizePresetDef>
<AIImages.ImageSizePresetDef>
<defName>Size_1024x1024</defName>
<label>1024x1024</label>
<width>1024</width>
<height>1024</height>
<category>Square</category>
<sortOrder>30</sortOrder>
</AIImages.ImageSizePresetDef>
<!-- Portrait Sizes -->
<AIImages.ImageSizePresetDef>
<defName>Size_512x768</defName>
<label>512x768</label>
<width>512</width>
<height>768</height>
<category>Portrait</category>
<sortOrder>40</sortOrder>
</AIImages.ImageSizePresetDef>
<AIImages.ImageSizePresetDef>
<defName>Size_768x1024</defName>
<label>768x1024</label>
<width>768</width>
<height>1024</height>
<category>Portrait</category>
<sortOrder>50</sortOrder>
</AIImages.ImageSizePresetDef>
<AIImages.ImageSizePresetDef>
<defName>Size_896x1152</defName>
<label>896x1152</label>
<width>896</width>
<height>1152</height>
<category>Portrait</category>
<sortOrder>60</sortOrder>
</AIImages.ImageSizePresetDef>
<!-- Landscape Sizes -->
<AIImages.ImageSizePresetDef>
<defName>Size_768x512</defName>
<label>768x512</label>
<width>768</width>
<height>512</height>
<category>Landscape</category>
<sortOrder>70</sortOrder>
</AIImages.ImageSizePresetDef>
<AIImages.ImageSizePresetDef>
<defName>Size_1024x768</defName>
<label>1024x768</label>
<width>1024</width>
<height>768</height>
<category>Landscape</category>
<sortOrder>80</sortOrder>
</AIImages.ImageSizePresetDef>
<AIImages.ImageSizePresetDef>
<defName>Size_1152x896</defName>
<label>1152x896</label>
<width>1152</width>
<height>1152</height>
<category>Landscape</category>
<sortOrder>90</sortOrder>
</AIImages.ImageSizePresetDef>
</Defs>

124
Defs/README.md Normal file
View File

@@ -0,0 +1,124 @@
# AI Images - Defs Documentation
This folder contains XML definition files that allow you to easily customize art styles and image size presets without recompiling the mod.
## Art Style Definitions (ArtStyleDefs.xml)
Art styles define how images should be generated, including prompts, quality tags, and negative prompts.
### Structure
```xml
<AIImages.ArtStyleDef>
<defName>ArtStyle_MyStyle</defName>
<label>My Custom Style</label>
<description>Description of the style</description>
<positivePrompt>style keywords here</positivePrompt>
<negativePrompt>things to avoid</negativePrompt>
<qualityTags>additional quality tags</qualityTags>
<addBaseQualityTags>true</addBaseQualityTags>
<addBaseNegativePrompts>true</addBaseNegativePrompts>
<sortOrder>100</sortOrder>
</AIImages.ArtStyleDef>
```
### Fields
- **defName**: Unique identifier (must start with `ArtStyle_`)
- **label**: Display name shown in the UI
- **description**: Tooltip text explaining the style
- **positivePrompt**: Keywords added to the positive prompt (e.g., "photorealistic, 8k uhd")
- **negativePrompt**: Keywords added to the negative prompt (e.g., "cartoon, anime")
- **qualityTags**: Style-specific quality tags
- **addBaseQualityTags**: If true, adds "highly detailed, professional, masterpiece, best quality"
- **addBaseNegativePrompts**: If true, adds base negative prompts like "ugly, deformed, low quality"
- **sortOrder**: Determines order in the UI (lower numbers appear first)
### Example: Custom Watercolor Style
```xml
<AIImages.ArtStyleDef>
<defName>ArtStyle_Watercolor</defName>
<label>Watercolor</label>
<description>Soft watercolor painting style</description>
<positivePrompt>watercolor painting, soft colors, flowing paint, artistic</positivePrompt>
<negativePrompt>photograph, digital art, sharp edges</negativePrompt>
<qualityTags>traditional art, paper texture</qualityTags>
<addBaseQualityTags>true</addBaseQualityTags>
<addBaseNegativePrompts>true</addBaseNegativePrompts>
<sortOrder>65</sortOrder>
</AIImages.ArtStyleDef>
```
## Image Size Presets (ImageSizePresetDefs.xml)
Image size presets provide quick buttons for common image dimensions.
### Structure
```xml
<AIImages.ImageSizePresetDef>
<defName>Size_1024x1024</defName>
<label>1024x1024</label>
<width>1024</width>
<height>1024</height>
<category>Square</category>
<sortOrder>30</sortOrder>
</AIImages.ImageSizePresetDef>
```
### Fields
- **defName**: Unique identifier (should start with `Size_`)
- **label**: Display text on the button
- **width**: Image width in pixels
- **height**: Image height in pixels
- **category**: Grouping category (Square, Portrait, Landscape, or custom)
- **sortOrder**: Determines button order (lower numbers appear first)
### Example: Ultra-wide Size
```xml
<AIImages.ImageSizePresetDef>
<defName>Size_2048x1024</defName>
<label>2048x1024</label>
<width>2048</width>
<height>1024</height>
<category>Ultrawide</category>
<sortOrder>95</sortOrder>
</AIImages.ImageSizePresetDef>
```
## Adding Custom Definitions
1. **Create a new XML file** in the `Defs` folder
2. **Start with the XML header**:
```xml
<?xml version="1.0" encoding="utf-8"?>
<Defs>
<!-- Your definitions here -->
</Defs>
```
3. **Add your definitions** using the structures above
4. **Restart RimWorld** to load the new definitions
## Tips
- Keep `defName` unique to avoid conflicts
- Use descriptive `label` values for the UI
- Adjust `sortOrder` to organize items logically
- Test your prompts with different characters to ensure good results
- For art styles, experiment with different combinations of tags
- Consider using existing styles as templates
## Compatibility
These definitions are compatible with other mods. If another mod adds art styles or size presets, they will all appear together in the UI.
## Troubleshooting
- **Style doesn't appear**: Check that `defName` is unique and starts with `ArtStyle_`
- **Size preset missing**: Verify the XML syntax and that `defName` starts with `Size_`
- **Prompts not working**: Make sure prompts are in English and follow Stable Diffusion prompt syntax
- **XML errors**: Use an XML validator to check your file for syntax errors

View File

@@ -7,6 +7,19 @@
<AIImages.Window.Title>AI Image Generator</AIImages.Window.Title> <AIImages.Window.Title>AI Image Generator</AIImages.Window.Title>
<AIImages.Window.PawnLabel>Character: {0}</AIImages.Window.PawnLabel> <AIImages.Window.PawnLabel>Character: {0}</AIImages.Window.PawnLabel>
<AIImages.Window.Refresh>Refresh</AIImages.Window.Refresh> <AIImages.Window.Refresh>Refresh</AIImages.Window.Refresh>
<AIImages.Copy>Copy</AIImages.Copy>
<!-- Character Info -->
<AIImages.CharacterInfo.Title>Character Information</AIImages.CharacterInfo.Title>
<AIImages.Info.Gender>Gender</AIImages.Info.Gender>
<AIImages.Info.Age>Age</AIImages.Info.Age>
<AIImages.Info.BodyType>Body Type</AIImages.Info.BodyType>
<AIImages.Info.SkinTone>Skin Tone</AIImages.Info.SkinTone>
<AIImages.Info.Hair>Hairstyle</AIImages.Info.Hair>
<AIImages.Info.HairColor>Hair Color</AIImages.Info.HairColor>
<AIImages.Info.Traits>Traits</AIImages.Info.Traits>
<AIImages.Info.Apparel>Apparel</AIImages.Info.Apparel>
<AIImages.Info.Genes>Genes</AIImages.Info.Genes>
<AIImages.Info.Hediffs>Hediffs</AIImages.Info.Hediffs>
<!-- Appearance --> <!-- Appearance -->
<AIImages.Appearance.SectionTitle>Appearance</AIImages.Appearance.SectionTitle> <AIImages.Appearance.SectionTitle>Appearance</AIImages.Appearance.SectionTitle>
<AIImages.Appearance.NoInfo>Appearance information unavailable</AIImages.Appearance.NoInfo> <AIImages.Appearance.NoInfo>Appearance information unavailable</AIImages.Appearance.NoInfo>
@@ -29,32 +42,42 @@
<AIImages.Apparel.ColorDesc> Color: {0}</AIImages.Apparel.ColorDesc> <AIImages.Apparel.ColorDesc> Color: {0}</AIImages.Apparel.ColorDesc>
<!-- Stable Diffusion Prompt --> <!-- Stable Diffusion Prompt -->
<AIImages.Prompt.SectionTitle>Stable Diffusion Prompt</AIImages.Prompt.SectionTitle> <AIImages.Prompt.SectionTitle>Stable Diffusion Prompt</AIImages.Prompt.SectionTitle>
<AIImages.Prompt.PositiveTitle>Positive Prompt</AIImages.Prompt.PositiveTitle>
<AIImages.Prompt.NegativeTitle>Negative Prompt</AIImages.Prompt.NegativeTitle>
<AIImages.Prompt.CopyButton>Copy Prompt</AIImages.Prompt.CopyButton> <AIImages.Prompt.CopyButton>Copy Prompt</AIImages.Prompt.CopyButton>
<AIImages.Prompt.CopyPositive>Copy Positive</AIImages.Prompt.CopyPositive>
<AIImages.Prompt.CopyNegative>Copy Negative</AIImages.Prompt.CopyNegative>
<AIImages.Prompt.Copied>Copied!</AIImages.Prompt.Copied> <AIImages.Prompt.Copied>Copied!</AIImages.Prompt.Copied>
<!-- Generation --> <!-- Generation -->
<AIImages.Generation.Generate>Generate Image</AIImages.Generation.Generate> <AIImages.Generation.Generate>Generate Image</AIImages.Generation.Generate>
<AIImages.Generation.Generating>Generating...</AIImages.Generation.Generating> <AIImages.Generation.Generating>Generating...</AIImages.Generation.Generating>
<AIImages.Generation.Cancel>Cancel Generation</AIImages.Generation.Cancel>
<AIImages.Generation.InProgress>Generating image, please wait...</AIImages.Generation.InProgress> <AIImages.Generation.InProgress>Generating image, please wait...</AIImages.Generation.InProgress>
<AIImages.Generation.Success>Image generated successfully!</AIImages.Generation.Success> <AIImages.Generation.Success>Image generated successfully!</AIImages.Generation.Success>
<AIImages.Generation.Failed>Generation failed</AIImages.Generation.Failed> <AIImages.Generation.Failed>Generation failed</AIImages.Generation.Failed>
<AIImages.Generation.Cancelled>Generation cancelled by user</AIImages.Generation.Cancelled>
<AIImages.Generation.Cancelling>Cancelling generation...</AIImages.Generation.Cancelling>
<AIImages.Generation.Error>Generation error</AIImages.Generation.Error>
<AIImages.Generation.SavedTo>Image saved to: {0}</AIImages.Generation.SavedTo> <AIImages.Generation.SavedTo>Image saved to: {0}</AIImages.Generation.SavedTo>
<AIImages.Generation.LoadedFromSave>Loaded saved portrait</AIImages.Generation.LoadedFromSave>
<AIImages.Generation.NoImage>No image generated yet.\nClick "Generate Image" to start.</AIImages.Generation.NoImage> <AIImages.Generation.NoImage>No image generated yet.\nClick "Generate Image" to start.</AIImages.Generation.NoImage>
<!-- Image Type -->
<AIImages.ImageType.Label>Image Type</AIImages.ImageType.Label>
<AIImages.ImageType.Portrait>Portrait</AIImages.ImageType.Portrait>
<AIImages.ImageType.FullBody>Full Body</AIImages.ImageType.FullBody>
<!-- Settings --> <!-- Settings -->
<AIImages.Settings.ApiSection>API Settings</AIImages.Settings.ApiSection> <AIImages.Settings.ApiSection>API Settings</AIImages.Settings.ApiSection>
<AIImages.Settings.ApiSectionTooltip>Configure connection to Stable Diffusion API</AIImages.Settings.ApiSectionTooltip> <AIImages.Settings.ApiSectionTooltip>Configure connection to Stable Diffusion API</AIImages.Settings.ApiSectionTooltip>
<AIImages.Settings.ApiEndpoint>API Endpoint</AIImages.Settings.ApiEndpoint> <AIImages.Settings.ApiEndpoint>API Endpoint</AIImages.Settings.ApiEndpoint>
<AIImages.Settings.TestConnection>Test Connection</AIImages.Settings.TestConnection> <AIImages.Settings.TestConnection>Test Connection</AIImages.Settings.TestConnection>
<AIImages.Settings.LoadModels>Load Available Models</AIImages.Settings.LoadModels> <AIImages.Settings.LoadFromApi>Load from API</AIImages.Settings.LoadFromApi>
<AIImages.Settings.Model>Model</AIImages.Settings.Model> <AIImages.Settings.Model>Model</AIImages.Settings.Model>
<AIImages.Settings.NoModelSelected>No model selected</AIImages.Settings.NoModelSelected> <AIImages.Settings.NoModelSelected>No model selected</AIImages.Settings.NoModelSelected>
<AIImages.Settings.LoadModelsFirst>Load models first</AIImages.Settings.LoadModelsFirst> <AIImages.Settings.LoadModelsFirst>Load models first</AIImages.Settings.LoadModelsFirst>
<AIImages.Settings.LoadSamplersSchedulers>Load Samplers &amp; Schedulers</AIImages.Settings.LoadSamplersSchedulers>
<AIImages.Settings.ConnectionSuccess>Successfully connected to API!</AIImages.Settings.ConnectionSuccess> <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.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.AllLoaded>Loaded {0} models, {1} samplers and {2} schedulers from API</AIImages.Settings.AllLoaded>
<AIImages.Settings.NoModelsFound>No models found. Check API connection.</AIImages.Settings.NoModelsFound> <AIImages.Settings.NothingLoaded>Nothing loaded. Check API connection.</AIImages.Settings.NothingLoaded>
<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.GenerationSection>Generation Settings</AIImages.Settings.GenerationSection>
<AIImages.Settings.GenerationSectionTooltip>Configure image generation parameters</AIImages.Settings.GenerationSectionTooltip> <AIImages.Settings.GenerationSectionTooltip>Configure image generation parameters</AIImages.Settings.GenerationSectionTooltip>
<AIImages.Settings.ArtStyle>Art Style</AIImages.Settings.ArtStyle> <AIImages.Settings.ArtStyle>Art Style</AIImages.Settings.ArtStyle>
@@ -73,5 +96,29 @@
<AIImages.Settings.AutoLoadModels>Auto-load models on startup</AIImages.Settings.AutoLoadModels> <AIImages.Settings.AutoLoadModels>Auto-load models on startup</AIImages.Settings.AutoLoadModels>
<AIImages.Settings.ShowTechnicalInfo>Show technical information</AIImages.Settings.ShowTechnicalInfo> <AIImages.Settings.ShowTechnicalInfo>Show technical information</AIImages.Settings.ShowTechnicalInfo>
<AIImages.Settings.SaveHistory>Save generation history</AIImages.Settings.SaveHistory> <AIImages.Settings.SaveHistory>Save generation history</AIImages.Settings.SaveHistory>
<AIImages.Settings.SaveImagesToServer>Save images to server</AIImages.Settings.SaveImagesToServer>
<AIImages.Settings.SaveImagesToServerTooltip>When enabled, images will also be saved to the Stable Diffusion server's output folder (in addition to the local save folder)</AIImages.Settings.SaveImagesToServerTooltip>
<AIImages.Settings.EnableDebugLogs>Enable debug logs</AIImages.Settings.EnableDebugLogs>
<AIImages.Settings.EnableDebugLogsTooltip>When enabled, detailed debug information will be logged to help troubleshoot issues</AIImages.Settings.EnableDebugLogsTooltip>
<AIImages.Settings.SavePath>Save Path</AIImages.Settings.SavePath> <AIImages.Settings.SavePath>Save Path</AIImages.Settings.SavePath>
<AIImages.Settings.ClearAllImages>Clear All Generated Images</AIImages.Settings.ClearAllImages>
<AIImages.Settings.ClearAllImagesConfirm>Are you sure you want to delete all generated portrait images? This action cannot be undone.</AIImages.Settings.ClearAllImagesConfirm>
<AIImages.Settings.ClearAllImagesSuccess>Successfully deleted {0} portrait image(s)</AIImages.Settings.ClearAllImagesSuccess>
<!-- Gallery -->
<AIImages.Gallery.Title>AI Images Gallery</AIImages.Gallery.Title>
<AIImages.Gallery.Count>Total images: {0}</AIImages.Gallery.Count>
<AIImages.Gallery.Empty>Gallery is empty</AIImages.Gallery.Empty>
<AIImages.Gallery.LoadError>Load error</AIImages.Gallery.LoadError>
<AIImages.Gallery.DeleteSelected>Delete selected</AIImages.Gallery.DeleteSelected>
<AIImages.Gallery.DeleteAll>Delete all ({0})</AIImages.Gallery.DeleteAll>
<AIImages.Gallery.ConfirmDelete>Delete selected image?</AIImages.Gallery.ConfirmDelete>
<AIImages.Gallery.ConfirmDeleteAll>Delete all {0} images? This action cannot be undone.</AIImages.Gallery.ConfirmDeleteAll>
<AIImages.Gallery.DeleteError>Delete error: {0}</AIImages.Gallery.DeleteError>
<AIImages.Gallery.Deleted>Image successfully deleted</AIImages.Gallery.Deleted>
<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> </LanguageData>

View File

@@ -7,6 +7,19 @@
<AIImages.Window.Title>Генератор AI Изображений</AIImages.Window.Title> <AIImages.Window.Title>Генератор AI Изображений</AIImages.Window.Title>
<AIImages.Window.PawnLabel>Персонаж: {0}</AIImages.Window.PawnLabel> <AIImages.Window.PawnLabel>Персонаж: {0}</AIImages.Window.PawnLabel>
<AIImages.Window.Refresh>Обновить</AIImages.Window.Refresh> <AIImages.Window.Refresh>Обновить</AIImages.Window.Refresh>
<AIImages.Copy>Копировать</AIImages.Copy>
<!-- Character Info -->
<AIImages.CharacterInfo.Title>Информация о персонаже</AIImages.CharacterInfo.Title>
<AIImages.Info.Gender>Пол</AIImages.Info.Gender>
<AIImages.Info.Age>Возраст</AIImages.Info.Age>
<AIImages.Info.BodyType>Тип тела</AIImages.Info.BodyType>
<AIImages.Info.SkinTone>Тон кожи</AIImages.Info.SkinTone>
<AIImages.Info.Hair>Прическа</AIImages.Info.Hair>
<AIImages.Info.HairColor>Цвет волос</AIImages.Info.HairColor>
<AIImages.Info.Traits>Черты характера</AIImages.Info.Traits>
<AIImages.Info.Apparel>Одежда</AIImages.Info.Apparel>
<AIImages.Info.Genes>Гены</AIImages.Info.Genes>
<AIImages.Info.Hediffs>Состояния</AIImages.Info.Hediffs>
<!-- Appearance --> <!-- Appearance -->
<AIImages.Appearance.SectionTitle>Внешность</AIImages.Appearance.SectionTitle> <AIImages.Appearance.SectionTitle>Внешность</AIImages.Appearance.SectionTitle>
<AIImages.Appearance.NoInfo>Информация о внешности недоступна</AIImages.Appearance.NoInfo> <AIImages.Appearance.NoInfo>Информация о внешности недоступна</AIImages.Appearance.NoInfo>
@@ -29,32 +42,42 @@
<AIImages.Apparel.ColorDesc> Цвет: {0}</AIImages.Apparel.ColorDesc> <AIImages.Apparel.ColorDesc> Цвет: {0}</AIImages.Apparel.ColorDesc>
<!-- Stable Diffusion Prompt --> <!-- Stable Diffusion Prompt -->
<AIImages.Prompt.SectionTitle>Промпт Stable Diffusion</AIImages.Prompt.SectionTitle> <AIImages.Prompt.SectionTitle>Промпт Stable Diffusion</AIImages.Prompt.SectionTitle>
<AIImages.Prompt.PositiveTitle>Позитивный промпт</AIImages.Prompt.PositiveTitle>
<AIImages.Prompt.NegativeTitle>Негативный промпт</AIImages.Prompt.NegativeTitle>
<AIImages.Prompt.CopyButton>Копировать промпт</AIImages.Prompt.CopyButton> <AIImages.Prompt.CopyButton>Копировать промпт</AIImages.Prompt.CopyButton>
<AIImages.Prompt.CopyPositive>Копировать позитивный</AIImages.Prompt.CopyPositive>
<AIImages.Prompt.CopyNegative>Копировать негативный</AIImages.Prompt.CopyNegative>
<AIImages.Prompt.Copied>Скопировано!</AIImages.Prompt.Copied> <AIImages.Prompt.Copied>Скопировано!</AIImages.Prompt.Copied>
<!-- Generation --> <!-- Generation -->
<AIImages.Generation.Generate>Сгенерировать изображение</AIImages.Generation.Generate> <AIImages.Generation.Generate>Сгенерировать изображение</AIImages.Generation.Generate>
<AIImages.Generation.Generating>Генерация...</AIImages.Generation.Generating> <AIImages.Generation.Generating>Генерация...</AIImages.Generation.Generating>
<AIImages.Generation.Cancel>Отменить генерацию</AIImages.Generation.Cancel>
<AIImages.Generation.InProgress>Генерируется изображение, пожалуйста подождите...</AIImages.Generation.InProgress> <AIImages.Generation.InProgress>Генерируется изображение, пожалуйста подождите...</AIImages.Generation.InProgress>
<AIImages.Generation.Success>Изображение успешно сгенерировано!</AIImages.Generation.Success> <AIImages.Generation.Success>Изображение успешно сгенерировано!</AIImages.Generation.Success>
<AIImages.Generation.Failed>Ошибка генерации</AIImages.Generation.Failed> <AIImages.Generation.Failed>Ошибка генерации</AIImages.Generation.Failed>
<AIImages.Generation.Cancelled>Генерация отменена пользователем</AIImages.Generation.Cancelled>
<AIImages.Generation.Cancelling>Отмена генерации...</AIImages.Generation.Cancelling>
<AIImages.Generation.Error>Ошибка генерации</AIImages.Generation.Error>
<AIImages.Generation.SavedTo>Изображение сохранено в: {0}</AIImages.Generation.SavedTo> <AIImages.Generation.SavedTo>Изображение сохранено в: {0}</AIImages.Generation.SavedTo>
<AIImages.Generation.LoadedFromSave>Загружен сохраненный портрет</AIImages.Generation.LoadedFromSave>
<AIImages.Generation.NoImage>Изображение еще не сгенерировано.\nНажмите "Сгенерировать изображение" для начала.</AIImages.Generation.NoImage> <AIImages.Generation.NoImage>Изображение еще не сгенерировано.\nНажмите "Сгенерировать изображение" для начала.</AIImages.Generation.NoImage>
<!-- Image Type -->
<AIImages.ImageType.Label>Тип изображения</AIImages.ImageType.Label>
<AIImages.ImageType.Portrait>Портрет</AIImages.ImageType.Portrait>
<AIImages.ImageType.FullBody>Полное тело</AIImages.ImageType.FullBody>
<!-- Settings --> <!-- Settings -->
<AIImages.Settings.ApiSection>Настройки API</AIImages.Settings.ApiSection> <AIImages.Settings.ApiSection>Настройки API</AIImages.Settings.ApiSection>
<AIImages.Settings.ApiSectionTooltip>Настройка подключения к API Stable Diffusion</AIImages.Settings.ApiSectionTooltip> <AIImages.Settings.ApiSectionTooltip>Настройка подключения к API Stable Diffusion</AIImages.Settings.ApiSectionTooltip>
<AIImages.Settings.ApiEndpoint>Адрес API</AIImages.Settings.ApiEndpoint> <AIImages.Settings.ApiEndpoint>Адрес API</AIImages.Settings.ApiEndpoint>
<AIImages.Settings.TestConnection>Проверить соединение</AIImages.Settings.TestConnection> <AIImages.Settings.TestConnection>Проверить соединение</AIImages.Settings.TestConnection>
<AIImages.Settings.LoadModels>Загрузить доступные модели</AIImages.Settings.LoadModels> <AIImages.Settings.LoadFromApi>Загрузить из API</AIImages.Settings.LoadFromApi>
<AIImages.Settings.Model>Модель</AIImages.Settings.Model> <AIImages.Settings.Model>Модель</AIImages.Settings.Model>
<AIImages.Settings.NoModelSelected>Модель не выбрана</AIImages.Settings.NoModelSelected> <AIImages.Settings.NoModelSelected>Модель не выбрана</AIImages.Settings.NoModelSelected>
<AIImages.Settings.LoadModelsFirst>Сначала загрузите модели</AIImages.Settings.LoadModelsFirst> <AIImages.Settings.LoadModelsFirst>Сначала загрузите модели</AIImages.Settings.LoadModelsFirst>
<AIImages.Settings.LoadSamplersSchedulers>Загрузить сэмплеры и планировщики</AIImages.Settings.LoadSamplersSchedulers>
<AIImages.Settings.ConnectionSuccess>Успешное подключение к API!</AIImages.Settings.ConnectionSuccess> <AIImages.Settings.ConnectionSuccess>Успешное подключение к API!</AIImages.Settings.ConnectionSuccess>
<AIImages.Settings.ConnectionFailed>Не удалось подключиться к API. Проверьте адрес и убедитесь, что Stable Diffusion WebUI запущен.</AIImages.Settings.ConnectionFailed> <AIImages.Settings.ConnectionFailed>Не удалось подключиться к API. Проверьте адрес и убедитесь, что Stable Diffusion WebUI запущен.</AIImages.Settings.ConnectionFailed>
<AIImages.Settings.ModelsLoaded>Загружено {0} моделей из API</AIImages.Settings.ModelsLoaded> <AIImages.Settings.AllLoaded>Загружено {0} моделей, {1} сэмплеров и {2} планировщиков из API</AIImages.Settings.AllLoaded>
<AIImages.Settings.NoModelsFound>Модели не найдены. Проверьте подключение к API.</AIImages.Settings.NoModelsFound> <AIImages.Settings.NothingLoaded>Ничего не загружено. Проверьте подключение к API.</AIImages.Settings.NothingLoaded>
<AIImages.Settings.SamplersSchedulersLoaded>Загружено {0} сэмплеров и {1} планировщиков из API</AIImages.Settings.SamplersSchedulersLoaded>
<AIImages.Settings.NoSamplersSchedulersFound>Сэмплеры и планировщики не найдены. Проверьте подключение к API.</AIImages.Settings.NoSamplersSchedulersFound>
<AIImages.Settings.GenerationSection>Настройки генерации</AIImages.Settings.GenerationSection> <AIImages.Settings.GenerationSection>Настройки генерации</AIImages.Settings.GenerationSection>
<AIImages.Settings.GenerationSectionTooltip>Настройка параметров генерации изображений</AIImages.Settings.GenerationSectionTooltip> <AIImages.Settings.GenerationSectionTooltip>Настройка параметров генерации изображений</AIImages.Settings.GenerationSectionTooltip>
<AIImages.Settings.ArtStyle>Художественный стиль</AIImages.Settings.ArtStyle> <AIImages.Settings.ArtStyle>Художественный стиль</AIImages.Settings.ArtStyle>
@@ -73,5 +96,29 @@
<AIImages.Settings.AutoLoadModels>Автоматически загружать модели при запуске</AIImages.Settings.AutoLoadModels> <AIImages.Settings.AutoLoadModels>Автоматически загружать модели при запуске</AIImages.Settings.AutoLoadModels>
<AIImages.Settings.ShowTechnicalInfo>Показывать техническую информацию</AIImages.Settings.ShowTechnicalInfo> <AIImages.Settings.ShowTechnicalInfo>Показывать техническую информацию</AIImages.Settings.ShowTechnicalInfo>
<AIImages.Settings.SaveHistory>Сохранять историю генераций</AIImages.Settings.SaveHistory> <AIImages.Settings.SaveHistory>Сохранять историю генераций</AIImages.Settings.SaveHistory>
<AIImages.Settings.SaveImagesToServer>Сохранять изображения на сервере</AIImages.Settings.SaveImagesToServer>
<AIImages.Settings.SaveImagesToServerTooltip>Если включено, изображения также будут сохранены в папке вывода сервера Stable Diffusion (помимо локальной папки сохранения)</AIImages.Settings.SaveImagesToServerTooltip>
<AIImages.Settings.EnableDebugLogs>Включить отладочные логи</AIImages.Settings.EnableDebugLogs>
<AIImages.Settings.EnableDebugLogsTooltip>При включении будет выводиться подробная отладочная информация для помощи в решении проблем</AIImages.Settings.EnableDebugLogsTooltip>
<AIImages.Settings.SavePath>Путь для сохранения</AIImages.Settings.SavePath> <AIImages.Settings.SavePath>Путь для сохранения</AIImages.Settings.SavePath>
<AIImages.Settings.ClearAllImages>Очистить все сгенерированные изображения</AIImages.Settings.ClearAllImages>
<AIImages.Settings.ClearAllImagesConfirm>Вы уверены, что хотите удалить все сгенерированные портреты? Это действие нельзя отменить.</AIImages.Settings.ClearAllImagesConfirm>
<AIImages.Settings.ClearAllImagesSuccess>Успешно удалено {0} изображений портретов</AIImages.Settings.ClearAllImagesSuccess>
<!-- Gallery -->
<AIImages.Gallery.Title>Галерея AI изображений</AIImages.Gallery.Title>
<AIImages.Gallery.Count>Всего изображений: {0}</AIImages.Gallery.Count>
<AIImages.Gallery.Empty>В галерее пока нет изображений</AIImages.Gallery.Empty>
<AIImages.Gallery.LoadError>Ошибка загрузки</AIImages.Gallery.LoadError>
<AIImages.Gallery.DeleteSelected>Удалить выбранное</AIImages.Gallery.DeleteSelected>
<AIImages.Gallery.DeleteAll>Удалить всё ({0})</AIImages.Gallery.DeleteAll>
<AIImages.Gallery.ConfirmDelete>Удалить выбранное изображение?</AIImages.Gallery.ConfirmDelete>
<AIImages.Gallery.ConfirmDeleteAll>Удалить все {0} изображений? Это действие нельзя отменить.</AIImages.Gallery.ConfirmDeleteAll>
<AIImages.Gallery.DeleteError>Ошибка удаления: {0}</AIImages.Gallery.DeleteError>
<AIImages.Gallery.Deleted>Изображение успешно удалено</AIImages.Gallery.Deleted>
<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> </LanguageData>

222
README.md
View File

@@ -0,0 +1,222 @@
# AI Images - RimWorld Mod
Мод для RimWorld, который генерирует AI-изображения персонажей (пешек) с помощью Stable Diffusion API.
## 🌟 Возможности
### 📸 Генерация AI-изображений
- **Автоматическое описание персонажей** на основе:
- Внешности (пол, возраст, тип тела, цвет кожи)
- Прически и цвета волос
- Одежды с материалами, качеством и цветами
- Черт характера
- Генов (биотехнологии)
- Состояний здоровья
### 🎨 Гибкая настройка стилей
- **9 предустановленных художественных стилей**:
- Реалистичный (Photorealistic)
- Полуреалистичный (Semi-Realistic)
- Аниме
- Концепт-арт
- Цифровая живопись
- Масляная живопись
- Эскиз
- Cel-shaded
- Без стиля (кастомные промпты)
- **Возможность создания собственных стилей** через XML-конфигурацию
- Базовые промпты и негативные промпты
- Автоматическое добавление качественных тегов
### 📐 Размеры изображений
- **Предустановки**: квадратные, портретные, ландшафтные
- **Кастомные размеры**: полный контроль над шириной и высотой
- **Два типа генерации**: портрет или полное тело
### 🖼️ Галерея изображений
- **Отдельная галерея** для каждого персонажа
- **Просмотр всех сгенерированных изображений** в удобной сетке
- **Выбор изображений** кликом по миниатюре
- **Удаление** отдельных или всех изображений
- **Отображение даты создания** и имени файла
### ⚙️ Настройки Stable Diffusion
- **Полная интеграция с Stable Diffusion WebUI**
- Загрузка моделей, сэмплеров и планировщиков из API
- Настройка количества шагов, CFG Scale, seed
- Превью промптов с возможностью копирования
- Прогресс-бар генерации с ETA
### 🎛️ Продвинутые функции
- **Автоматическое сохранение** изображений
- **История генераций** для каждого персонажа
- **Обратная совместимость** со старыми сохранениями
- **Двуязычный интерфейс** (русский/английский)
- **Отладочные логи** для диагностики
## 🚀 Установка
### Требования
- **RimWorld** версии 1.6
- **Harmony** (автоматически подтягивается через зависимости)
- **Stable Diffusion WebUI** (локально или удаленно)
### Зависимости
Мод использует следующие зависимости (автоматически подтягиваются):
- `brrainz.harmony` - Harmony
- `rim.job.world` - RimJobWorld (необязательно, но в зависимостях)
### Шаги установки
1. Клонируйте репозиторий или скачайте релиз
2. Скопируйте папку `ai-images` в директорию `Mods` RimWorld
3. Запустите Stable Diffusion WebUI
4. В настройках RimWorld включите мод AI Images
5. В настройках мода укажите адрес API Stable Diffusion (по умолчанию `http://127.0.0.1:7860`)
## 📖 Использование
### Генерация изображений
1. **Откройте окно AI Images**:
- Нажмите на кнопку "AI Портрет" в панели действий персонажа
2. **Настройте параметры**:
- Выберите художественный стиль
- Выберите тип изображения (портрет/полное тело)
- Настройте размер изображения
- При необходимости измените другие параметры
3. **Сгенерируйте изображение**:
- Нажмите кнопку "Сгенерировать изображение"
- Дождитесь завершения генерации
- Изображение автоматически сохранится
### Просмотр галереи
1. **Откройте галерею**:
- В окне генерации нажмите кнопку "Открыть галерею"
- Откроется окно со всеми изображениями персонажа
2. **Управление изображениями**:
- Кликните по изображению для выбора
- Нажмите "Удалить выбранное" для удаления конкретного изображения
- Нажмите "Удалить всё" для очистки галереи
### Настройка стилей
Мод использует XML-конфигурацию для определения стилей. Вы можете создать свои собственные стили, редактируя файл `Defs/ArtStyleDefs.xml`.
Пример создания нового стиля:
```xml
<AIImages.ArtStyleDef>
<defName>ArtStyle_MyCustomStyle</defName>
<label>Мой Кастомный Стиль</label>
<description>Описание вашего стиля</description>
<positivePrompt>ваши ключевые слова здесь</positivePrompt>
<negativePrompt>чего избегать</negativePrompt>
<qualityTags>дополнительные теги качества</qualityTags>
<addBaseQualityTags>true</addBaseQualityTags>
<addBaseNegativePrompts>true</addBaseNegativePrompts>
<sortOrder>100</sortOrder>
</AIImages.ArtStyleDef>
```
Подробнее о конфигурации стилей в [Defs/README.md](Defs/README.md).
## 🎯 Особенности
### Умная генерация промптов
Мод автоматически анализирует персонажа и создает детальные промпты для Stable Diffusion:
- Описывает цвет кожи естественным языком
- Добавляет информацию об одежде с материалами и качествами
- Учитывает черты характера персонажа
- Включает данные о генах из Biotech DLC
- Адаптирует промпт в зависимости от типа изображения
### Оптимизация
- Асинхронная генерация без блокировки игры
- Возможность отмены генерации
- Прогресс-бар с реальным временем выполнения
- Эффективное управление памятью для текстур
### Обратная совместимость
Старые сохранения с одним портретом автоматически мигрируют в новую систему галереи.
## 🛠️ Разработка
### Структура проекта
```
ai-images/
├── About/ # Метаданные мода
├── Assemblies/ # Скомпилированные DLL
├── Defs/ # XML-конфигурации (стили, размеры)
├── Languages/ # Переводы
├── Source/ # Исходный код
│ └── AIImages/
│ ├── Components/ # Компоненты пешек
│ ├── Defs/ # Классы определений
│ ├── Helpers/ # Вспомогательные классы
│ ├── Models/ # Модели данных
│ ├── Patches/ # Harmony патчи
│ ├── Services/ # Бизнес-логика
│ ├── Settings/ # Настройки мода
│ ├── UI/ # Пользовательский интерфейс
│ └── Window_AIImage.cs # Главное окно
└── Textures/ # Текстуры UI
```
### Сборка
```bash
cd Source/AIImages
dotnet build -c Release
```
### Языки
Мод поддерживает английский и русский языки. Переводы находятся в `Languages/`.
## 📝 История версий
### v1.0.0
- Базовая генерация AI-изображений
- Интеграция с Stable Diffusion API
- 9 художественных стилей
- Галерея изображений
- Настройка размеров
- Двуязычный интерфейс
## 🤝 Вклад
Приветствуются любые вклады! Пожалуйста:
1. Форкните репозиторий
2. Создайте ветку для вашей функции
3. Закоммитьте изменения
4. Отправьте Pull Request
## 📄 Лицензия
См. файл [LICENSE](LICENSE) для деталей.
## 🙏 Благодарности
- Ludeon Studios за создание RimWorld
- Automattic1111 за Stable Diffusion WebUI
- Сообщество RimWorld за поддержку
## ⚠️ Известные ограничения
- Требует запущенный Stable Diffusion WebUI
- Генерация изображений может занимать время в зависимости от настроек
- Некоторые модели Stable Diffusion могут работать медленнее других
## 🐛 Сообщение об ошибках
Если вы нашли баг или хотите предложить улучшение, пожалуйста, создайте Issue на GitHub с подробным описанием.
---
**Автор**: mrleo1nid
**Версия RimWorld**: 1.6
**Версия мода**: 1.0.0

View File

@@ -18,6 +18,6 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Krafs.Rimworld.Ref" Version="1.6.4566" /> <PackageReference Include="Krafs.Rimworld.Ref" Version="1.6.4566" />
<PackageReference Include="Lib.Harmony" Version="2.4.1" /> <PackageReference Include="Lib.Harmony" Version="2.4.1" />
<PackageReference Include="StableDiffusionNet" Version="1.0.1" /> <PackageReference Include="StableDiffusionNet.Core" Version="1.1.5" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,39 +1,91 @@
using System;
using AIImages.Services; using AIImages.Services;
using AIImages.Settings; using AIImages.Settings;
using AIImages.Validation;
using HarmonyLib; using HarmonyLib;
using RimWorld;
using UnityEngine; using UnityEngine;
using Verse; using Verse;
namespace AIImages namespace AIImages
{ {
/// <summary> /// <summary>
/// Main mod class with settings support /// Main mod class with settings support and dependency injection
/// </summary> /// </summary>
public class AIImagesMod : Mod public class AIImagesMod : Mod
{ {
public static AIImagesModSettings Settings { get; private set; } private static AIImagesMod _instance = null!;
private readonly ServiceContainer _serviceContainer;
// Singleton сервисы /// <summary>
public static IPawnDescriptionService PawnDescriptionService { get; private set; } /// Глобальный экземпляр мода (для доступа из других классов)
public static IPromptGeneratorService PromptGeneratorService { get; private set; } /// </summary>
public static IStableDiffusionApiService ApiService { get; private set; } public static AIImagesMod Instance
{
get
{
if (_instance == null)
{
throw new InvalidOperationException(
"[AI Images] Mod instance not initialized. This should not happen."
);
}
return _instance;
}
private set => _instance = value;
}
/// <summary>
/// Настройки мода
/// </summary>
public static AIImagesModSettings Settings => Instance._serviceContainer.Settings;
/// <summary>
/// Контейнер сервисов с dependency injection
/// </summary>
public static ServiceContainer Services => Instance._serviceContainer;
public AIImagesMod(ModContentPack content) public AIImagesMod(ModContentPack content)
: base(content) : base(content)
{ {
Settings = GetSettings<AIImagesModSettings>(); Instance = this;
// Инициализируем сервисы var settings = GetSettings<AIImagesModSettings>();
PawnDescriptionService = new PawnDescriptionService();
PromptGeneratorService = new AdvancedPromptGenerator();
ApiService = new StableDiffusionApiService(Settings.savePath);
Log.Message("[AI Images] Mod initialized successfully with settings"); // Валидируем настройки при загрузке
var validationResult = SettingsValidator.Validate(settings);
if (!validationResult.IsValid)
{
Log.Warning(
$"[AI Images] Settings validation failed:\n{validationResult.GetErrorsAsString()}"
);
}
if (validationResult.HasWarnings)
{
Log.Warning(
$"[AI Images] Settings validation warnings:\n{validationResult.GetWarningsAsString()}"
);
}
// Создаем контейнер сервисов
try
{
_serviceContainer = new ServiceContainer(settings);
Log.Message("[AI Images] Mod initialized successfully with dependency injection");
}
catch (Exception ex)
{
Log.Error(
$"[AI Images] Failed to initialize ServiceContainer: {ex.Message}\n{ex.StackTrace}"
);
throw;
}
} }
public override void DoSettingsWindowContents(Rect inRect) public override void DoSettingsWindowContents(Rect inRect)
{ {
AIImagesSettingsUI.DoSettingsWindowContents(inRect, Settings); AIImagesSettingsUI.DoSettingsWindowContents(inRect, Settings, _serviceContainer);
base.DoSettingsWindowContents(inRect); base.DoSettingsWindowContents(inRect);
} }
@@ -41,6 +93,27 @@ namespace AIImages
{ {
return "AI Images"; return "AI Images";
} }
/// <summary>
/// Вызывается при выгрузке мода
/// </summary>
public override void WriteSettings()
{
base.WriteSettings();
// Валидируем настройки перед сохранением
var validationResult = SettingsValidator.Validate(Settings);
if (!validationResult.IsValid)
{
Log.Warning(
$"[AI Images] Saving settings with validation errors:\n{validationResult.GetErrorsAsString()}"
);
Messages.Message(
"AI Images: Some settings have validation errors. Check the log.",
MessageTypeDefOf.CautionInput
);
}
}
} }
/// <summary> /// <summary>

View File

@@ -0,0 +1,117 @@
using System.Collections.Generic;
using System.Linq;
using AIImages.Helpers;
using Verse;
namespace AIImages.Components
{
/// <summary>
/// Компонент для хранения данных AI-сгенерированных портретов пешки
/// </summary>
public class PawnPortraitComp : ThingComp
{
/// <summary>
/// Список путей к сохраненным портретам (галерея)
/// </summary>
private List<string> portraitPaths = new List<string>();
/// <summary>
/// Есть ли сохраненные портреты
/// </summary>
public bool HasPortrait => portraitPaths != null && portraitPaths.Count > 0;
/// <summary>
/// Количество портретов в галерее
/// </summary>
public int PortraitCount => portraitPaths?.Count ?? 0;
/// <summary>
/// Получить все пути к портретам
/// </summary>
public List<string> GetAllPortraits() => portraitPaths?.ToList() ?? new List<string>();
/// <summary>
/// Получить последний портрет (для обратной совместимости)
/// </summary>
public string PortraitPath => HasPortrait ? portraitPaths.Last() : null;
/// <summary>
/// Добавить новый портрет в галерею
/// </summary>
public void AddPortrait(string path)
{
if (string.IsNullOrEmpty(path))
return;
if (portraitPaths == null)
portraitPaths = new List<string>();
portraitPaths.Add(path);
DebugLogger.Log($"[AI Images] Added portrait to gallery: {path}");
}
/// <summary>
/// Удалить портрет из галереи
/// </summary>
public bool RemovePortrait(string path)
{
if (portraitPaths == null || string.IsNullOrEmpty(path))
return false;
bool removed = portraitPaths.Remove(path);
if (removed)
{
DebugLogger.Log($"[AI Images] Removed portrait from gallery: {path}");
}
return removed;
}
/// <summary>
/// Очистить все портреты
/// </summary>
public void ClearPortraits()
{
if (portraitPaths != null)
{
int count = portraitPaths.Count;
portraitPaths.Clear();
DebugLogger.Log($"[AI Images] Cleared {count} portraits from gallery");
}
}
/// <summary>
/// Сохранение/загрузка данных
/// </summary>
public override void PostExposeData()
{
base.PostExposeData();
bool isSaving = Scribe.mode == LoadSaveMode.Saving;
bool isLoading = Scribe.mode == LoadSaveMode.LoadingVars;
DebugLogger.Log(
$"[AI Images] PostExposeData for {parent?.LabelShort} - Mode: {Scribe.mode}, Portrait count: {PortraitCount}"
);
// Сохраняем список портретов
Scribe_Collections.Look(ref portraitPaths, "aiPortraitPaths", LookMode.Value);
// Обратная совместимость: если есть старый формат с одним портретом, добавляем его в список
if (isLoading && (portraitPaths == null || portraitPaths.Count == 0))
{
string oldPortraitPath = null;
Scribe_Values.Look(ref oldPortraitPath, "aiPortraitPath", null);
if (!string.IsNullOrEmpty(oldPortraitPath))
{
portraitPaths = new List<string> { oldPortraitPath };
DebugLogger.Log($"[AI Images] Migrated old single portrait to gallery: {oldPortraitPath}");
}
}
DebugLogger.Log(
$"[AI Images] PostExposeData completed for {parent?.LabelShort} - Portrait count: {PortraitCount}, HasPortrait: {HasPortrait}"
);
}
}
}

View File

@@ -0,0 +1,46 @@
using System.Diagnostics.CodeAnalysis;
using Verse;
namespace AIImages
{
/// <summary>
/// Определение художественного стиля для генерации изображений
/// </summary>
[SuppressMessage(
"Major Code Smell",
"S1104:Fields should not have public accessibility",
Justification = "Required for RimWorld's Def system XML serialization"
)]
public class ArtStyleDef : Def
{
/// <summary>
/// Промпт для позитивного описания стиля
/// </summary>
public string positivePrompt = "";
/// <summary>
/// Промпт для негативного описания (что исключить)
/// </summary>
public string negativePrompt = "";
/// <summary>
/// Теги качества специфичные для этого стиля
/// </summary>
public string qualityTags = "";
/// <summary>
/// Добавлять ли базовые теги качества (highly detailed, professional, masterpiece, best quality)
/// </summary>
public bool addBaseQualityTags = true;
/// <summary>
/// Добавлять ли базовые негативные промпты (ugly, deformed, low quality, etc.)
/// </summary>
public bool addBaseNegativePrompts = true;
/// <summary>
/// Порядок сортировки в UI
/// </summary>
public int sortOrder = 100;
}
}

View File

@@ -0,0 +1,36 @@
using System.Diagnostics.CodeAnalysis;
using Verse;
namespace AIImages
{
/// <summary>
/// Предустановка размера изображения
/// </summary>
[SuppressMessage(
"Major Code Smell",
"S1104:Fields should not have public accessibility",
Justification = "Required for RimWorld's Def system XML serialization"
)]
public class ImageSizePresetDef : Def
{
/// <summary>
/// Ширина изображения в пикселях
/// </summary>
public int width = 512;
/// <summary>
/// Высота изображения в пикселях
/// </summary>
public int height = 512;
/// <summary>
/// Категория размера (Square, Portrait, Landscape)
/// </summary>
public string category = "Square";
/// <summary>
/// Порядок сортировки в UI
/// </summary>
public int sortOrder = 100;
}
}

View File

@@ -0,0 +1,145 @@
using System;
using System.Threading.Tasks;
using RimWorld;
using Verse;
namespace AIImages.Helpers
{
/// <summary>
/// Вспомогательный класс для правильной обработки асинхронных операций в RimWorld
/// Предотвращает fire-and-forget паттерн и обеспечивает централизованную обработку ошибок
/// </summary>
public static class AsyncHelper
{
/// <summary>
/// Выполняет асинхронную задачу с обработкой ошибок
/// </summary>
public static async Task RunAsync(Func<Task> taskFunc, string operationName = "Operation")
{
try
{
await taskFunc();
}
catch (OperationCanceledException)
{
Log.Message($"[AI Images] {operationName} was cancelled");
Messages.Message($"{operationName} was cancelled", MessageTypeDefOf.RejectInput);
}
catch (Exception ex)
{
Log.Error($"[AI Images] Error in {operationName}: {ex.Message}\n{ex.StackTrace}");
Messages.Message(
$"Error in {operationName}: {ex.Message}",
MessageTypeDefOf.RejectInput
);
}
}
/// <summary>
/// Выполняет асинхронную задачу с обработкой ошибок и callback при успехе
/// </summary>
public static async Task RunAsync<T>(
Func<Task<T>> taskFunc,
Action<T> onSuccess,
string operationName = "Operation"
)
{
try
{
T result = await taskFunc();
onSuccess?.Invoke(result);
}
catch (OperationCanceledException)
{
Log.Message($"[AI Images] {operationName} was cancelled");
Messages.Message($"{operationName} was cancelled", MessageTypeDefOf.RejectInput);
}
catch (Exception ex)
{
Log.Error($"[AI Images] Error in {operationName}: {ex.Message}\n{ex.StackTrace}");
Messages.Message(
$"Error in {operationName}: {ex.Message}",
MessageTypeDefOf.RejectInput
);
}
}
/// <summary>
/// Выполняет асинхронную задачу с полным контролем: onSuccess, onError, onCancel
/// </summary>
public static async Task RunAsync<T>(
Func<Task<T>> taskFunc,
Action<T> onSuccess,
Action<Exception> onError = null,
Action onCancel = null,
string operationName = "Operation"
)
{
try
{
T result = await taskFunc();
onSuccess?.Invoke(result);
}
catch (OperationCanceledException)
{
Log.Message($"[AI Images] {operationName} was cancelled");
if (onCancel != null)
{
onCancel();
}
else
{
Messages.Message(
$"{operationName} was cancelled",
MessageTypeDefOf.RejectInput
);
}
}
catch (Exception ex)
{
Log.Error($"[AI Images] Error in {operationName}: {ex.Message}\n{ex.StackTrace}");
if (onError != null)
{
onError(ex);
}
else
{
Messages.Message(
$"Error in {operationName}: {ex.Message}",
MessageTypeDefOf.RejectInput
);
}
}
}
/// <summary>
/// Безопасно выполняет Task без ожидания результата, с логированием ошибок
/// Используется когда нужен fire-and-forget, но с обработкой ошибок
/// </summary>
public static void FireAndForget(Task task, string operationName = "Background operation")
{
if (task == null)
return;
task.ContinueWith(
t =>
{
if (t.IsFaulted && t.Exception != null)
{
var ex = t.Exception.GetBaseException();
Log.Error(
$"[AI Images] Error in {operationName}: {ex.Message}\n{ex.StackTrace}"
);
}
else if (t.IsCanceled)
{
Log.Message($"[AI Images] {operationName} was cancelled");
}
},
TaskScheduler.Default
);
}
}
}

View File

@@ -0,0 +1,43 @@
using Verse;
namespace AIImages.Helpers
{
/// <summary>
/// Вспомогательный класс для условного отладочного логирования
/// </summary>
public static class DebugLogger
{
/// <summary>
/// Логирует сообщение только если включены отладочные логи
/// </summary>
public static void Log(string message)
{
if (AIImagesMod.Settings?.enableDebugLogs == true)
{
Verse.Log.Message(message);
}
}
/// <summary>
/// Логирует предупреждение только если включены отладочные логи
/// </summary>
public static void Warning(string message)
{
if (AIImagesMod.Settings?.enableDebugLogs == true)
{
Verse.Log.Warning(message);
}
}
/// <summary>
/// Логирует ошибку только если включены отладочные логи
/// </summary>
public static void Error(string message)
{
if (AIImagesMod.Settings?.enableDebugLogs == true)
{
Verse.Log.Error(message);
}
}
}
}

View File

@@ -0,0 +1,249 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AIImages.Components;
using UnityEngine;
using Verse;
namespace AIImages.Helpers
{
/// <summary>
/// Вспомогательный класс для работы с портретами персонажей
/// </summary>
public static class PawnPortraitHelper
{
/// <summary>
/// Получить компонент портрета пешки
/// </summary>
public static PawnPortraitComp GetPortraitComp(Pawn pawn)
{
if (pawn == null)
{
DebugLogger.Warning("[AI Images] GetPortraitComp called with null pawn");
return null;
}
var comp = pawn.TryGetComp<PawnPortraitComp>();
DebugLogger.Log(
$"[AI Images] GetPortraitComp for {pawn.Name}: {(comp != null ? "Found" : "Not found")}"
);
return comp;
}
/// <summary>
/// Сохранить путь к портрету на пешке (добавляет в галерею)
/// </summary>
public static void SavePortraitPath(Pawn pawn, string path)
{
DebugLogger.Log(
$"[AI Images] SavePortraitPath called for {pawn?.Name} with path: {path}"
);
var comp = GetPortraitComp(pawn);
if (comp != null)
{
comp.AddPortrait(path);
DebugLogger.Log(
$"[AI Images] Successfully added portrait path for {pawn.Name}: {path}"
);
DebugLogger.Log($"[AI Images] Component now has {comp.PortraitCount} portraits");
}
else
{
DebugLogger.Error($"[AI Images] Failed to get portrait component for {pawn?.Name}");
}
}
/// <summary>
/// Получить путь к портрету пешки
/// </summary>
public static string GetPortraitPath(Pawn pawn)
{
DebugLogger.Log($"[AI Images] GetPortraitPath called for {pawn?.Name}");
var comp = GetPortraitComp(pawn);
if (comp == null)
{
DebugLogger.Warning($"[AI Images] No portrait component found for {pawn?.Name}");
return null;
}
DebugLogger.Log(
$"[AI Images] Portrait component found for {pawn?.Name}, path: '{comp.PortraitPath}'"
);
return comp.PortraitPath;
}
/// <summary>
/// Есть ли у пешки сохраненный портрет
/// </summary>
public static bool HasPortrait(Pawn pawn)
{
DebugLogger.Log($"[AI Images] HasPortrait called for {pawn?.Name}");
var comp = GetPortraitComp(pawn);
if (comp == null)
{
DebugLogger.Log($"[AI Images] No portrait component found for {pawn?.Name}");
return false;
}
bool hasPortrait = comp.HasPortrait;
DebugLogger.Log(
$"[AI Images] Portrait component for {pawn?.Name} has portrait: {hasPortrait} (path: '{comp.PortraitPath}')"
);
return hasPortrait;
}
/// <summary>
/// Загрузить портрет пешки как текстуру
/// </summary>
public static Texture2D LoadPortrait(Pawn pawn)
{
DebugLogger.Log($"[AI Images] LoadPortrait called for {pawn?.Name}");
string path = GetPortraitPath(pawn);
DebugLogger.Log($"[AI Images] Retrieved path for {pawn?.Name}: '{path}'");
if (string.IsNullOrEmpty(path))
{
DebugLogger.Log($"[AI Images] No portrait path found for {pawn?.Name}");
return null;
}
if (!File.Exists(path))
{
DebugLogger.Warning(
$"[AI Images] Portrait file does not exist for {pawn?.Name}: {path}"
);
return null;
}
DebugLogger.Log($"[AI Images] Portrait file exists for {pawn?.Name}: {path}");
try
{
byte[] imageData = File.ReadAllBytes(path);
DebugLogger.Log(
$"[AI Images] Successfully read {imageData.Length} bytes from {path}"
);
Texture2D texture = new Texture2D(2, 2);
texture.LoadImage(imageData);
DebugLogger.Log(
$"[AI Images] Successfully loaded texture for {pawn?.Name}, size: {texture.width}x{texture.height}"
);
return texture;
}
catch (System.Exception ex)
{
DebugLogger.Warning(
$"[AI Images] Failed to load portrait for {pawn?.Name}: {ex.Message}"
);
return null;
}
}
/// <summary>
/// Очистить портрет пешки (удаляет все портреты)
/// </summary>
public static void ClearPortrait(Pawn pawn)
{
var comp = GetPortraitComp(pawn);
if (comp != null)
{
comp.ClearPortraits();
}
}
/// <summary>
/// Получить все пути к портретам пешки (галерея)
/// </summary>
public static List<string> GetAllPortraits(Pawn pawn)
{
var comp = GetPortraitComp(pawn);
return comp?.GetAllPortraits() ?? new List<string>();
}
/// <summary>
/// Получить количество портретов в галерее
/// </summary>
public static int GetPortraitCount(Pawn pawn)
{
var comp = GetPortraitComp(pawn);
return comp?.PortraitCount ?? 0;
}
/// <summary>
/// Удалить конкретный портрет из галереи
/// </summary>
public static bool RemovePortrait(Pawn pawn, string path)
{
var comp = GetPortraitComp(pawn);
return comp?.RemovePortrait(path) ?? false;
}
/// <summary>
/// Очистить все сгенерированные портреты
/// </summary>
public static int ClearAllPortraits(string savePath)
{
ClearPawnComponents();
return DeletePortraitFiles(savePath);
}
private static void ClearPawnComponents()
{
if (Current.Game?.Maps == null)
return;
foreach (var map in Current.Game.Maps)
{
foreach (var pawn in map.mapPawns.AllPawns)
{
var comp = GetPortraitComp(pawn);
if (comp != null && comp.HasPortrait)
{
comp.ClearPortraits();
}
}
}
}
private static int DeletePortraitFiles(string savePath)
{
int deletedCount = 0;
string fullPath = Path.Combine(GenFilePaths.SaveDataFolderPath, savePath);
if (!Directory.Exists(fullPath))
return deletedCount;
try
{
var files = Directory.GetFiles(fullPath, "*.png");
deletedCount = files.Count(TryDeleteFile);
Log.Message($"[AI Images] Deleted {deletedCount} portrait files");
}
catch (System.Exception ex)
{
Log.Error($"[AI Images] Error cleaning portraits directory: {ex.Message}");
}
return deletedCount;
}
private static bool TryDeleteFile(string file)
{
try
{
File.Delete(file);
return true;
}
catch (System.Exception ex)
{
Log.Warning($"[AI Images] Failed to delete file {file}: {ex.Message}");
return false;
}
}
}
}

View File

@@ -15,6 +15,7 @@ namespace AIImages.Models
public string Scheduler { get; set; } public string Scheduler { get; set; }
public int Seed { get; set; } public int Seed { get; set; }
public string Model { get; set; } public string Model { get; set; }
public bool SaveImagesToServer { get; set; }
} }
/// <summary> /// <summary>
@@ -48,4 +49,40 @@ namespace AIImages.Models
}; };
} }
} }
/// <summary>
/// Прогресс генерации изображения
/// </summary>
public class GenerationProgress
{
/// <summary>
/// Процент завершения (0.0 - 1.0)
/// </summary>
public double Progress { get; set; }
/// <summary>
/// Текущий шаг
/// </summary>
public int CurrentStep { get; set; }
/// <summary>
/// Общее количество шагов
/// </summary>
public int TotalSteps { get; set; }
/// <summary>
/// Оставшееся время в секундах (приблизительно)
/// </summary>
public double EtaRelative { get; set; }
/// <summary>
/// Идет ли генерация в данный момент
/// </summary>
public bool IsActive { get; set; }
/// <summary>
/// Превью текущего состояния генерации (base64)
/// </summary>
public string CurrentImage { get; set; }
}
} }

View File

@@ -15,16 +15,21 @@ namespace AIImages.Models
public int Age { get; set; } public int Age { get; set; }
public string BodyType { get; set; } public string BodyType { get; set; }
public Color SkinColor { get; set; } public Color SkinColor { get; set; }
public List<string> SkinColorGeneDefNames { get; set; }
public List<string> HairColorGeneDefNames { get; set; }
public string HairStyle { get; set; } public string HairStyle { get; set; }
public string HairDefName { get; set; } public string HairDefName { get; set; }
public Color HairColor { get; set; } public Color HairColor { get; set; }
public List<Trait> Traits { get; set; } public List<Trait> Traits { get; set; }
public List<ApparelData> Apparel { get; set; } public List<ApparelData> Apparel { get; set; }
public string EventDescription { get; set; }
public PawnAppearanceData() public PawnAppearanceData()
{ {
Traits = new List<Trait>(); Traits = new List<Trait>();
Apparel = new List<ApparelData>(); Apparel = new List<ApparelData>();
SkinColorGeneDefNames = new List<string>();
HairColorGeneDefNames = new List<string>();
} }
} }

View File

@@ -1,5 +1,14 @@
namespace AIImages.Models namespace AIImages.Models
{ {
/// <summary>
/// Тип генерации изображения
/// </summary>
public enum ImageType
{
Portrait, // Портрет
FullBody, // Полное тело
}
/// <summary> /// <summary>
/// Настройки для генерации изображений через Stable Diffusion /// Настройки для генерации изображений через Stable Diffusion
/// </summary> /// </summary>
@@ -15,7 +24,8 @@ namespace AIImages.Models
public string Scheduler { get; set; } public string Scheduler { get; set; }
public int Seed { get; set; } public int Seed { get; set; }
public string Model { get; set; } public string Model { get; set; }
public ArtStyle ArtStyle { get; set; } public string ArtStyleDefName { get; set; }
public ImageType ImageType { get; set; }
public StableDiffusionSettings() public StableDiffusionSettings()
{ {
@@ -25,27 +35,12 @@ namespace AIImages.Models
Width = 512; Width = 512;
Height = 768; Height = 768;
Sampler = "Euler a"; Sampler = "Euler a";
Scheduler = "Automatic"; Scheduler = "Automatic"; // С большой буквы для API
Seed = -1; // Случайный seed Seed = -1; // Случайный seed
ArtStyle = ArtStyle.Realistic; ArtStyleDefName = "ArtStyle_Realistic";
PositivePrompt = ""; PositivePrompt = "";
NegativePrompt = "ugly, deformed, low quality, blurry, bad anatomy, worst quality"; NegativePrompt = "ugly, deformed, low quality, blurry, bad anatomy, worst quality";
ImageType = ImageType.Portrait;
} }
} }
/// <summary>
/// Художественный стиль изображения
/// </summary>
public enum ArtStyle
{
None, // Без стиля
Realistic,
SemiRealistic,
Anime,
ConceptArt,
DigitalPainting,
OilPainting,
Sketch,
CellShaded,
}
} }

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

@@ -0,0 +1,65 @@
using System.Collections.Generic;
using System.Reflection;
using AIImages.Components;
using AIImages.Helpers;
using HarmonyLib;
using Verse;
namespace AIImages.Patches
{
/// <summary>
/// Патч для добавления PawnPortraitComp ко всем пешкам
/// </summary>
[HarmonyPatch(typeof(ThingWithComps), nameof(ThingWithComps.InitializeComps))]
public static class PawnPortraitCompPatch
{
private static FieldInfo allCompsField = AccessTools.Field(typeof(ThingWithComps), "comps");
[HarmonyPostfix]
public static void AddPortraitComp(ThingWithComps __instance)
{
// Проверяем, является ли объект пешкой-гуманоидом и нет ли уже компонента
if (
__instance is Pawn pawn
&& pawn.RaceProps?.Humanlike == true
&& pawn.GetComp<PawnPortraitComp>() == null
)
{
DebugLogger.Log($"[AI Images] Adding portrait component to {pawn.Name}");
// Создаем компонент
var comp = new PawnPortraitComp { parent = pawn };
// Инициализируем компонент
comp.Initialize(null);
// Получаем список компонентов через рефлексию и добавляем наш
var compsList = allCompsField.GetValue(pawn) as List<ThingComp>;
if (compsList != null)
{
compsList.Add(comp);
DebugLogger.Log(
$"[AI Images] Successfully added portrait component to {pawn.Name}"
);
}
else
{
DebugLogger.Error($"[AI Images] Failed to get comps list for {pawn.Name}");
}
}
else if (__instance is Pawn pawn2)
{
if (pawn2.RaceProps?.Humanlike != true)
{
DebugLogger.Log($"[AI Images] Skipping non-humanlike pawn: {pawn2.Name}");
}
else if (pawn2.GetComp<PawnPortraitComp>() != null)
{
DebugLogger.Log(
$"[AI Images] Portrait component already exists for {pawn2.Name}"
);
}
}
}
}
}

View File

@@ -10,7 +10,7 @@ using Verse;
namespace AIImages namespace AIImages
{ {
/// <summary> /// <summary>
/// Harmony patch to add a gizmo (button) to all colonist pawns /// Harmony patch to add a gizmo (button) to all pawns
/// </summary> /// </summary>
[HarmonyPatch(typeof(Pawn), "GetGizmos")] [HarmonyPatch(typeof(Pawn), "GetGizmos")]
[System.Diagnostics.CodeAnalysis.SuppressMessage( [System.Diagnostics.CodeAnalysis.SuppressMessage(
@@ -34,12 +34,8 @@ namespace AIImages
yield return gizmo; yield return gizmo;
} }
// Only add button to colonist pawns that are spawned // Add button to all spawned pawns (not just colonists)
if ( if (__instance.Spawned)
__instance.IsColonist
&& __instance.Spawned
&& __instance.Faction == Faction.OfPlayer
)
{ {
yield return new Command_Action yield return new Command_Action
{ {

View File

@@ -34,25 +34,6 @@ namespace AIImages.Services
{ "Pretty", "attractive features, pleasant appearance, charming" }, { "Pretty", "attractive features, pleasant appearance, charming" },
}; };
private static readonly Dictionary<ArtStyle, string> ArtStylePrompts = new Dictionary<
ArtStyle,
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" },
{
ArtStyle.ConceptArt,
"concept art, digital art, artstation, professional concept design"
},
{ ArtStyle.DigitalPainting, "digital painting, painterly, brush strokes, artistic" },
{ ArtStyle.OilPainting, "oil painting, traditional painting, canvas, fine art" },
{ ArtStyle.Sketch, "pencil sketch, hand drawn, sketch art, line art" },
{ ArtStyle.CellShaded, "cell shaded, flat colors, toon shading, stylized" },
};
public string GeneratePositivePrompt( public string GeneratePositivePrompt(
PawnAppearanceData appearanceData, PawnAppearanceData appearanceData,
StableDiffusionSettings settings StableDiffusionSettings settings
@@ -63,24 +44,36 @@ namespace AIImages.Services
StringBuilder prompt = new StringBuilder(); StringBuilder prompt = new StringBuilder();
// 1. Художественный стиль // 1. Базовый пользовательский промпт (если указан) - идет первым
if ( if (!string.IsNullOrEmpty(settings.PositivePrompt))
ArtStylePrompts.TryGetValue(settings.ArtStyle, out string stylePrompt)
&& !string.IsNullOrEmpty(stylePrompt)
)
{ {
prompt.Append(stylePrompt); prompt.Append(settings.PositivePrompt.TrimEnd(',', ' '));
prompt.Append(", "); prompt.Append(", ");
} }
// 2. Тип кадра - автоматически добавляем "portrait" для генерации персонажей // 2. Художественный стиль
prompt.Append("portrait, head and shoulders of "); var styleDef = DefDatabase<ArtStyleDef>.GetNamedSilentFail(settings.ArtStyleDefName);
if (styleDef != null && !string.IsNullOrEmpty(styleDef.positivePrompt))
{
prompt.Append(styleDef.positivePrompt);
prompt.Append(", ");
}
// 3. Базовое описание (возраст и пол) // 3. Тип кадра (portrait или full body)
prompt.Append(GetAgeAndGenderDescription(appearanceData)); string frameType =
settings.ImageType == Models.ImageType.FullBody ? "full body, " : "portrait, ";
prompt.Append(frameType);
// 4. Пол персонажа (в формате anime/SD теги)
string genderTag = appearanceData.Gender == Gender.Female ? "1girl" : "1boy";
prompt.Append(genderTag);
prompt.Append(", "); prompt.Append(", ");
// 4. Тип тела // 5. Точный возраст
prompt.Append($"{appearanceData.Age} y.o.");
prompt.Append(", ");
// 6. Тип тела
string bodyType = GetBodyTypeDescription(appearanceData.BodyType); string bodyType = GetBodyTypeDescription(appearanceData.BodyType);
if (!string.IsNullOrEmpty(bodyType)) if (!string.IsNullOrEmpty(bodyType))
{ {
@@ -88,14 +81,15 @@ namespace AIImages.Services
prompt.Append(", "); prompt.Append(", ");
} }
// 5. Цвет кожи // 7. Цвет кожи (сначала проверяем гены, затем используем цвет как fallback)
string skinTone = ColorDescriptionService.GetSkinToneDescription( string skinTone = GetSkinToneDescription(appearanceData);
appearanceData.SkinColor if (!string.IsNullOrEmpty(skinTone))
); {
prompt.Append(skinTone); prompt.Append(skinTone);
prompt.Append(", "); prompt.Append(", ");
}
// 6. Волосы // 8. Волосы
string hairDescription = GetHairDescription(appearanceData); string hairDescription = GetHairDescription(appearanceData);
if (!string.IsNullOrEmpty(hairDescription)) if (!string.IsNullOrEmpty(hairDescription))
{ {
@@ -103,7 +97,7 @@ namespace AIImages.Services
prompt.Append(", "); prompt.Append(", ");
} }
// 7. Настроение и выражение на основе черт характера // 9. Настроение и выражение на основе черт характера
string moodDescription = GetMoodFromTraits(appearanceData.Traits); string moodDescription = GetMoodFromTraits(appearanceData.Traits);
if (!string.IsNullOrEmpty(moodDescription)) if (!string.IsNullOrEmpty(moodDescription))
{ {
@@ -111,7 +105,7 @@ namespace AIImages.Services
prompt.Append(", "); prompt.Append(", ");
} }
// 8. Одежда // 10. Одежда
string apparelDescription = GetApparelDescription(appearanceData.Apparel); string apparelDescription = GetApparelDescription(appearanceData.Apparel);
if (!string.IsNullOrEmpty(apparelDescription)) if (!string.IsNullOrEmpty(apparelDescription))
{ {
@@ -119,53 +113,78 @@ namespace AIImages.Services
prompt.Append(", "); prompt.Append(", ");
} }
// 9. Базовый пользовательский промпт (если указан) // 11. Качественные теги
if (!string.IsNullOrEmpty(settings.PositivePrompt)) prompt.Append(GetQualityTags(settings.ArtStyleDefName));
{
prompt.Append(settings.PositivePrompt);
prompt.Append(", ");
}
// 10. Качественные теги
prompt.Append(GetQualityTags(settings.ArtStyle));
return prompt.ToString().Trim().TrimEnd(','); 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) public string GenerateNegativePrompt(StableDiffusionSettings settings)
{ {
StringBuilder negativePrompt = new StringBuilder(); StringBuilder negativePrompt = new StringBuilder();
// Базовые негативные промпты // 1. Пользовательский негативный промпт (если указан) - идет первым
if (!string.IsNullOrEmpty(settings.NegativePrompt))
{
negativePrompt.Append(settings.NegativePrompt.TrimEnd(',', ' '));
}
// Получаем стиль из Def
var styleDef = DefDatabase<ArtStyleDef>.GetNamedSilentFail(settings.ArtStyleDefName);
if (styleDef == null || !styleDef.addBaseNegativePrompts)
{
// Для стилей без базовых негативов - используем только пользовательский промпт
return negativePrompt.ToString().Trim();
}
// 2. Базовые негативные промпты
if (negativePrompt.Length > 0)
{
negativePrompt.Append(", ");
}
negativePrompt.Append( negativePrompt.Append(
"ugly, deformed, low quality, blurry, bad anatomy, worst quality, " "ugly, deformed, low quality, blurry, bad anatomy, worst quality, "
); );
negativePrompt.Append( negativePrompt.Append(
"mutated, disfigured, bad proportions, extra limbs, missing limbs, " "mutated, disfigured, bad proportions, extra limbs, missing limbs"
); );
// Специфичные для стиля негативы // 3. Специфичные для стиля негативы из Def
switch (settings.ArtStyle) if (!string.IsNullOrEmpty(styleDef.negativePrompt))
{ {
case ArtStyle.Realistic: negativePrompt.Append(", ");
case ArtStyle.SemiRealistic: negativePrompt.Append(styleDef.negativePrompt);
negativePrompt.Append("cartoon, anime, painting, drawing, illustration, ");
break;
case ArtStyle.Anime:
negativePrompt.Append("realistic, photo, photography, 3d, ");
break;
case ArtStyle.None:
// Без дополнительных негативных промптов для стиля None
break;
} }
// Пользовательский негативный промпт return negativePrompt.ToString().Trim();
if (!string.IsNullOrEmpty(settings.NegativePrompt))
{
negativePrompt.Append(settings.NegativePrompt);
}
return negativePrompt.ToString().Trim().TrimEnd(',');
} }
public string GetFullPromptDescription( public string GetFullPromptDescription(
@@ -195,27 +214,6 @@ namespace AIImages.Services
return description.ToString(); return description.ToString();
} }
private string GetAgeAndGenderDescription(PawnAppearanceData data)
{
string ageGroup = data.Age switch
{
< 18 => "young",
< 25 => "young adult",
< 35 => "adult",
< 50 => "middle-aged",
< 65 => "mature",
_ => "elderly",
};
string genderLabel = data.Gender switch
{
Gender.Male => "man",
Gender.Female => "woman",
_ => "person",
};
return $"{ageGroup} {genderLabel}";
}
private string GetBodyTypeDescription(string bodyType) private string GetBodyTypeDescription(string bodyType)
{ {
if (string.IsNullOrEmpty(bodyType)) if (string.IsNullOrEmpty(bodyType))
@@ -232,6 +230,79 @@ namespace AIImages.Services
}; };
} }
private string GetSkinToneDescription(PawnAppearanceData data)
{
// Сначала проверяем специальные гены цвета кожи (зелёная, синяя и т.д.)
if (data.SkinColorGeneDefNames != null && data.SkinColorGeneDefNames.Any())
{
// Ищем специальные гены (зелёная, синяя, красная и т.д.)
foreach (var geneDefName in data.SkinColorGeneDefNames)
{
string specialSkinTone = GetSpecialGeneSkinTone(geneDefName);
if (!string.IsNullOrEmpty(specialSkinTone))
return specialSkinTone;
}
}
// Если нет специальных генов, используем вычисленный RimWorld цвет
return ColorDescriptionService.GetSkinToneDescription(data.SkinColor);
}
/// <summary>
/// Получает описание для специальных генов цвета кожи
/// </summary>
private string GetSpecialGeneSkinTone(string geneDefName)
{
switch (geneDefName)
{
case "Skin_InkBlack":
return "ink black skin";
case "Skin_SlateGray":
return "slate gray skin";
case "Skin_LightGray":
return "light gray skin";
case "Skin_SheerWhite":
return "sheer white skin";
case "Skin_Blue":
return "blue skin";
case "Skin_Purple":
return "purple skin";
case "Skin_PaleRed":
return "pale red skin";
case "Skin_DeepRed":
return "deep red skin";
case "Skin_PaleYellow":
return "pale yellow skin";
case "Skin_DeepYellow":
return "deep yellow skin";
case "Skin_Orange":
return "orange skin";
case "Skin_Green":
return "green skin";
default:
// Пытаемся определить по названию (defName всегда на английском)
if (geneDefName.Contains("Green"))
return "green skin";
if (geneDefName.Contains("Blue"))
return "blue skin";
if (geneDefName.Contains("Red"))
return "red skin";
if (geneDefName.Contains("Yellow"))
return "yellow skin";
if (geneDefName.Contains("Purple"))
return "purple skin";
if (geneDefName.Contains("Orange"))
return "orange skin";
if (geneDefName.Contains("Black"))
return "black skin";
if (geneDefName.Contains("White"))
return "white skin";
if (geneDefName.Contains("Gray") || geneDefName.Contains("Grey"))
return "gray skin";
return null;
}
}
private string GetHairDescription(PawnAppearanceData data) private string GetHairDescription(PawnAppearanceData data)
{ {
if (string.IsNullOrEmpty(data.HairDefName)) if (string.IsNullOrEmpty(data.HairDefName))
@@ -239,10 +310,15 @@ namespace AIImages.Services
StringBuilder hair = new StringBuilder(); StringBuilder hair = new StringBuilder();
// Цвет волос // Цвет волос - сначала проверяем гены
string hairColor = ColorDescriptionService.GetHairColorDescription(data.HairColor); string hairColor = GetHairColorFromData(data);
if (string.IsNullOrEmpty(hairColor))
{
// Если нет генов, используем цвет из RGB
hairColor = ColorDescriptionService.GetHairColorDescription(data.HairColor);
}
hair.Append(hairColor); hair.Append(hairColor);
hair.Append(" "); hair.Append(" hair, ");
// Стиль прически - используем DefName для английского названия // Стиль прически - используем DefName для английского названия
string style = CleanDefName(data.HairDefName) string style = CleanDefName(data.HairDefName)
@@ -255,6 +331,94 @@ namespace AIImages.Services
return hair.ToString(); return hair.ToString();
} }
/// <summary>
/// Получает описание цвета волос из генов в данных персонажа
/// </summary>
private string GetHairColorFromData(PawnAppearanceData data)
{
if (data?.HairColorGeneDefNames == null || !data.HairColorGeneDefNames.Any())
return null;
// Берем первый ген (они уже отсортированы по приоритету)
string geneDefName = data.HairColorGeneDefNames.FirstOrDefault();
if (string.IsNullOrEmpty(geneDefName))
return null;
// Определяем цвет волос по названию гена
return GetHairColorFromGeneDefName(geneDefName);
}
/// <summary>
/// Получает описание цвета волос из названия гена
/// </summary>
private string GetHairColorFromGeneDefName(string geneDefName)
{
if (string.IsNullOrEmpty(geneDefName))
return null;
// Специфичные описания для известных генов цвета волос
switch (geneDefName)
{
case "Hair_Blond":
case "Hair_Blonde":
return "blond";
case "Hair_Brown":
return "brown";
case "Hair_Black":
return "black";
case "Hair_White":
return "white";
case "Hair_Red":
return "red";
case "Hair_Ginger":
return "ginger";
case "Hair_Auburn":
return "auburn";
case "Hair_Copper":
case "Hair_CopperBrown":
return "copper-brown";
case "Hair_Light":
return "light";
case "Hair_Dark":
return "dark";
default:
// Пытаемся определить по названию (defName всегда на английском)
if (geneDefName.Contains("Blond"))
return "blond";
if (geneDefName.Contains("Brown"))
return "brown";
if (geneDefName.Contains("Black"))
return "black";
if (geneDefName.Contains("White"))
return "white";
if (geneDefName.Contains("Red"))
return "red";
if (geneDefName.Contains("Ginger"))
return "ginger";
if (geneDefName.Contains("Auburn"))
return "auburn";
if (geneDefName.Contains("Copper"))
return "copper-brown";
if (geneDefName.Contains("Orange"))
return "orange-red";
if (geneDefName.Contains("Light"))
return "light";
if (geneDefName.Contains("Dark"))
return "dark";
if (geneDefName.Contains("Blue"))
return "blue";
if (geneDefName.Contains("Green"))
return "green";
if (geneDefName.Contains("Purple"))
return "purple";
if (geneDefName.Contains("Pink"))
return "pink";
if (geneDefName.Contains("Gray") || geneDefName.Contains("Grey"))
return "gray";
return null;
}
}
private string GetMoodFromTraits(List<Trait> traits) private string GetMoodFromTraits(List<Trait> traits)
{ {
if (traits == null || !traits.Any()) if (traits == null || !traits.Any())
@@ -316,24 +480,33 @@ namespace AIImages.Services
return apparelDesc.ToString(); return apparelDesc.ToString();
} }
private string GetQualityTags(ArtStyle style) private string GetQualityTags(string styleDefName)
{ {
var baseTags = "highly detailed, professional, masterpiece, best quality"; var styleDef = DefDatabase<ArtStyleDef>.GetNamedSilentFail(styleDefName);
if (styleDef == null)
switch (style)
{ {
case ArtStyle.None: return "";
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;
} }
StringBuilder tags = new StringBuilder();
// Базовые теги качества
if (styleDef.addBaseQualityTags)
{
tags.Append("highly detailed, professional, masterpiece, best quality");
}
// Специфичные для стиля теги качества
if (!string.IsNullOrEmpty(styleDef.qualityTags))
{
if (tags.Length > 0)
{
tags.Append(", ");
}
tags.Append(styleDef.qualityTags);
}
return tags.ToString();
} }
/// <summary> /// <summary>

View File

@@ -1,4 +1,8 @@
using System.Linq;
using System.Reflection;
using RimWorld;
using UnityEngine; using UnityEngine;
using Verse;
namespace AIImages.Services namespace AIImages.Services
{ {
@@ -7,8 +11,43 @@ namespace AIImages.Services
/// </summary> /// </summary>
public static class ColorDescriptionService public static class ColorDescriptionService
{ {
// Константы для повторяющихся строковых литералов
private const string Green = "Green";
private const string Orange = "Orange";
private const string Black = "Black";
private const string White = "White";
/// <summary> /// <summary>
/// Получает текстовое описание цвета волос /// Получает текстовое описание цвета волос
/// Сначала проверяет гены цвета волос, затем использует вычисленный RimWorld цвет волос
/// </summary>
public static string GetHairColorDescription(Pawn pawn)
{
if (pawn?.genes?.GenesListForReading != null)
{
// Ищем активные и неподавленные гены цвета волос
var hairColorGenes = pawn
.genes.GenesListForReading.Where(gene =>
gene.Active && !IsGeneSuppressed(pawn, gene) && IsHairColorGene(gene.def)
)
.ToList();
// Если есть гены цвета волос, используем их описание
if (hairColorGenes.Any())
{
var hairColorGene = hairColorGenes[0];
string geneDescription = GetHairColorGeneDescription(hairColorGene.def);
if (!string.IsNullOrEmpty(geneDescription))
return geneDescription;
}
}
// Иначе используем вычисленный RimWorld цвет волос
return GetHairColorDescription(pawn?.story?.HairColor ?? Color.white);
}
/// <summary>
/// Получает текстовое описание цвета волос по RGB цвету (fallback метод)
/// </summary> /// </summary>
public static string GetHairColorDescription(Color color) public static string GetHairColorDescription(Color color)
{ {
@@ -32,6 +71,37 @@ namespace AIImages.Services
/// <summary> /// <summary>
/// Получает текстовое описание цвета кожи /// Получает текстовое описание цвета кожи
/// Сначала проверяет специальные гены цвета кожи (зелёная, синяя и т.д.),
/// затем использует вычисленный RimWorld цвет кожи
/// </summary>
public static string GetSkinToneDescription(Pawn pawn)
{
if (pawn?.genes?.GenesListForReading == null)
return GetSkinToneDescription(pawn.story.SkinColor);
// Ищем активные и неподавленные гены цвета кожи (особенно специальные: зелёная, синяя, красная и т.д.)
var skinColorGenes = pawn
.genes.GenesListForReading.Where(gene =>
gene.Active && !IsGeneSuppressed(pawn, gene) && IsSpecialSkinColorGene(gene.def)
)
.ToList();
// Если есть специальные гены цвета кожи, используем их описание
if (skinColorGenes.Any())
{
var specialGene = skinColorGenes[0];
string geneDescription = GetSpecialGeneSkinTone(specialGene.def.defName);
if (!string.IsNullOrEmpty(geneDescription))
return geneDescription;
}
// Иначе RimWorld уже определил цвет текстуры пешки на основе генов (включая уровень мелатонина)
// Используем этот вычисленный цвет
return GetSkinToneDescription(pawn.story.SkinColor);
}
/// <summary>
/// Получает текстовое описание цвета кожи по RGB цвету (fallback метод)
/// </summary> /// </summary>
public static string GetSkinToneDescription(Color color) public static string GetSkinToneDescription(Color color)
{ {
@@ -59,6 +129,349 @@ namespace AIImages.Services
return "very dark skin"; return "very dark skin";
} }
/// <summary>
/// Проверяет, является ли ген специальным геном цвета кожи (зелёная, синяя, красная и т.д.)
/// Исключает гены меланина (Melanin1-9), так как они определяют обычный цвет кожи
/// </summary>
private static bool IsSpecialSkinColorGene(GeneDef geneDef)
{
if (geneDef == null)
return false;
string geneDefName = geneDef.defName;
// Исключаем гены меланина - они не дают специальный цвет
if (geneDefName.StartsWith("Skin_Melanin"))
return false;
// Специальные цвета: зелёная, синяя, красная, жёлтая, фиолетовая, оранжевая, чёрная, белая, серая
return geneDefName.StartsWith("Skin_") && HasSpecialSkinColorKeyword(geneDefName);
}
/// <summary>
/// Проверяет наличие ключевых слов специальных цветов кожи
/// </summary>
private static bool HasSpecialSkinColorKeyword(string geneDefName)
{
return geneDefName.Contains(Green)
|| geneDefName.Contains("Blue")
|| geneDefName.Contains("Red")
|| geneDefName.Contains("Yellow")
|| geneDefName.Contains("Purple")
|| geneDefName.Contains(Orange)
|| geneDefName.Contains(Black)
|| geneDefName.Contains(White)
|| geneDefName.Contains("Gray")
|| geneDefName.Contains("Grey")
|| geneDefName.Contains("Ink")
|| geneDefName.Contains("Slate")
|| geneDefName.Contains("Sheer")
|| geneDefName.Contains("Pale")
|| geneDefName.Contains("Deep");
}
/// <summary>
/// Получает описание для специальных генов цвета кожи
/// </summary>
private static string GetSpecialGeneSkinTone(string geneDefName)
{
// Сначала проверяем точные совпадения
string exactMatch = GetExactSkinColorMatch(geneDefName);
if (exactMatch != null)
return exactMatch;
// Затем пытаемся определить по ключевым словам
return GetSkinColorByKeyword(geneDefName);
}
/// <summary>
/// Получает точное совпадение для известных генов цвета кожи
/// </summary>
private static string GetExactSkinColorMatch(string geneDefName)
{
switch (geneDefName)
{
case "Skin_InkBlack":
return "ink black skin";
case "Skin_SlateGray":
return "slate gray skin";
case "Skin_LightGray":
return "light gray skin";
case "Skin_SheerWhite":
return "sheer white skin";
case "Skin_Blue":
return "blue skin";
case "Skin_Purple":
return "purple skin";
case "Skin_PaleRed":
return "pale red skin";
case "Skin_DeepRed":
return "deep red skin";
case "Skin_PaleYellow":
return "pale yellow skin";
case "Skin_DeepYellow":
return "deep yellow skin";
case "Skin_Orange":
return "orange skin";
case "Skin_Green":
return "green skin";
default:
return null;
}
}
/// <summary>
/// Определяет цвет кожи по ключевым словам в названии гена
/// </summary>
private static string GetSkinColorByKeyword(string geneDefName)
{
// Пытаемся определить по названию (defName всегда на английском)
if (geneDefName.Contains(Green))
return "green skin";
if (geneDefName.Contains("Blue"))
return "blue skin";
if (geneDefName.Contains("Red"))
return "red skin";
if (geneDefName.Contains("Yellow"))
return "yellow skin";
if (geneDefName.Contains("Purple"))
return "purple skin";
if (geneDefName.Contains(Orange))
return "orange skin";
if (geneDefName.Contains(Black))
return "black skin";
if (geneDefName.Contains(White))
return "white skin";
if (geneDefName.Contains("Gray") || geneDefName.Contains("Grey"))
return "gray skin";
return null;
}
/// <summary>
/// Проверяет, подавлен ли ген другим активным геном
/// </summary>
private static bool IsGeneSuppressed(Pawn pawn, Gene gene)
{
if (pawn?.genes == null || gene?.def == null)
return false;
// В RimWorld, если ген активен, он уже учтён в системе
// Но мы можем проверить, подавляет ли другой активный ген данный ген
// Используем GeneUtility для проверки подавления
try
{
// Проверяем все активные гены на предмет подавления данного гена
foreach (var otherGene in pawn.genes.GenesListForReading)
{
if (
otherGene.Active
&& otherGene != gene
&& otherGene.def != null
&& CheckGeneSuppression(otherGene.def, gene.def)
)
{
return true;
}
}
}
catch
{
// Если reflection не работает, возвращаем false
// В RimWorld свойство Active уже учитывает подавление
return false;
}
return false;
}
/// <summary>
/// Проверяет, подавляет ли один ген другой через reflection
/// </summary>
private static bool CheckGeneSuppression(GeneDef suppressor, GeneDef suppressed)
{
var suppressesField = suppressor.GetType().GetField("suppresses");
if (suppressesField == null)
return false;
var suppresses = suppressesField.GetValue(suppressor);
if (suppresses is System.Collections.Generic.List<GeneDef> suppressesList)
{
return suppressesList.Contains(suppressed);
}
if (suppresses is System.Collections.Generic.IEnumerable<GeneDef> suppressesEnum)
{
return suppressesEnum.Contains(suppressed);
}
return false;
}
/// <summary>
/// Проверяет, является ли ген геном цвета волос
/// </summary>
private static bool IsHairColorGene(GeneDef geneDef)
{
if (geneDef == null)
return false;
string geneDefName = geneDef.defName;
// Проверяем стандартные паттерны для генов цвета волос
return geneDefName.StartsWith("Hair_")
|| geneDefName.Contains("HairColor")
|| geneDefName.Contains("HairColour")
|| geneDefName.Contains("HairTone")
|| (
geneDefName.Contains("Hair")
&& (
geneDefName.Contains("Color")
|| geneDefName.Contains("Colour")
|| geneDefName.Contains("Red")
|| geneDefName.Contains("Blue")
|| geneDefName.Contains(Green)
|| geneDefName.Contains("Blond")
|| geneDefName.Contains("Brown")
|| geneDefName.Contains(Black)
|| geneDefName.Contains(White)
|| geneDefName.Contains("Light")
|| geneDefName.Contains("Dark")
|| geneDefName.Contains("Ginger")
|| geneDefName.Contains("Auburn")
|| geneDefName.Contains("Copper")
|| geneDefName.Contains(Orange)
)
);
}
/// <summary>
/// Получает описание цвета волос из гена
/// </summary>
private static string GetHairColorGeneDescription(GeneDef geneDef)
{
if (geneDef == null)
return null;
string geneDefName = geneDef.defName;
// Сначала проверяем точные совпадения
string exactMatch = GetExactHairColorMatch(geneDefName);
if (exactMatch != null)
return exactMatch;
// Затем пытаемся определить по ключевым словам
return GetHairColorByKeyword(geneDefName);
}
/// <summary>
/// Получает точное совпадение для известных генов цвета волос
/// </summary>
private static string GetExactHairColorMatch(string geneDefName)
{
switch (geneDefName)
{
case "Hair_Blond":
case "Hair_Blonde":
return "blond hair";
case "Hair_Brown":
return "brown hair";
case "Hair_Black":
return "black hair";
case "Hair_White":
return "white hair";
case "Hair_Red":
return "red hair";
case "Hair_Ginger":
return "ginger hair";
case "Hair_Auburn":
return "auburn hair";
case "Hair_Copper":
case "Hair_CopperBrown":
return "copper-brown hair";
case "Hair_Light":
return "light hair";
case "Hair_Dark":
return "dark hair";
default:
return null;
}
}
/// <summary>
/// Определяет цвет волос по ключевым словам в названии гена
/// </summary>
private static string GetHairColorByKeyword(string geneDefName)
{
// Проверяем основные цвета волос
string basicColor = GetBasicHairColor(geneDefName);
if (basicColor != null)
return basicColor;
// Проверяем специальные цвета
string specialColor = GetSpecialHairColor(geneDefName);
if (specialColor != null)
return specialColor;
// Проверяем оттенки
return GetHairColorShade(geneDefName);
}
/// <summary>
/// Получает основной цвет волос
/// </summary>
private static string GetBasicHairColor(string geneDefName)
{
if (geneDefName.Contains("Blond"))
return "blond hair";
if (geneDefName.Contains("Brown"))
return "brown hair";
if (geneDefName.Contains(Black))
return "black hair";
if (geneDefName.Contains(White))
return "white hair";
if (geneDefName.Contains("Red"))
return "red hair";
if (geneDefName.Contains("Ginger"))
return "ginger hair";
if (geneDefName.Contains("Auburn"))
return "auburn hair";
return null;
}
/// <summary>
/// Получает специальный цвет волос
/// </summary>
private static string GetSpecialHairColor(string geneDefName)
{
if (geneDefName.Contains("Copper"))
return "copper-brown hair";
if (geneDefName.Contains(Orange))
return "orange-red hair";
if (geneDefName.Contains("Blue"))
return "blue hair";
if (geneDefName.Contains(Green))
return "green hair";
if (geneDefName.Contains("Purple"))
return "purple hair";
if (geneDefName.Contains("Pink"))
return "pink hair";
if (geneDefName.Contains("Gray") || geneDefName.Contains("Grey"))
return "gray hair";
return null;
}
/// <summary>
/// Получает оттенок волос
/// </summary>
private static string GetHairColorShade(string geneDefName)
{
if (geneDefName.Contains("Light"))
return "light hair";
if (geneDefName.Contains("Dark"))
return "dark hair";
return null;
}
/// <summary> /// <summary>
/// Получает описание цвета одежды /// Получает описание цвета одежды
/// </summary> /// </summary>

View File

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

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AIImages.Models; using AIImages.Models;
@@ -12,26 +13,46 @@ namespace AIImages.Services
/// <summary> /// <summary>
/// Генерирует изображение на основе запроса /// Генерирует изображение на основе запроса
/// </summary> /// </summary>
Task<GenerationResult> GenerateImageAsync(GenerationRequest request); Task<GenerationResult> GenerateImageAsync(
GenerationRequest request,
CancellationToken cancellationToken = default
);
/// <summary>
/// Получает прогресс текущей генерации
/// </summary>
Task<GenerationProgress> GetProgressAsync(CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// Проверяет доступность API /// Проверяет доступность API
/// </summary> /// </summary>
Task<bool> CheckApiAvailability(string apiEndpoint); Task<bool> CheckApiAvailability(
string apiEndpoint,
CancellationToken cancellationToken = default
);
/// <summary> /// <summary>
/// Получает список доступных моделей с API /// Получает список доступных моделей с API
/// </summary> /// </summary>
Task<List<string>> GetAvailableModels(string apiEndpoint); Task<List<string>> GetAvailableModels(
string apiEndpoint,
CancellationToken cancellationToken = default
);
/// <summary> /// <summary>
/// Получает список доступных сэмплеров /// Получает список доступных сэмплеров
/// </summary> /// </summary>
Task<List<string>> GetAvailableSamplers(string apiEndpoint); Task<List<string>> GetAvailableSamplers(
string apiEndpoint,
CancellationToken cancellationToken = default
);
/// <summary> /// <summary>
/// Получает список доступных schedulers /// Получает список доступных schedulers
/// </summary> /// </summary>
Task<List<string>> GetAvailableSchedulers(string apiEndpoint); Task<List<string>> GetAvailableSchedulers(
string apiEndpoint,
CancellationToken cancellationToken = default
);
} }
} }

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Text; using System.Text;
using AIImages.Models; using AIImages.Models;
using RimWorld; using RimWorld;
@@ -29,6 +30,31 @@ namespace AIImages.Services
HairColor = pawn.story.HairColor, HairColor = pawn.story.HairColor,
}; };
// Извлекаем гены цвета кожи
if (pawn.genes?.GenesListForReading != null)
{
var skinColorGenes = pawn
.genes.GenesListForReading
.Where(gene => gene.Active && IsSkinColorGene(gene.def) && !IsGeneSuppressed(pawn, gene))
.ToList();
foreach (var gene in skinColorGenes)
{
data.SkinColorGeneDefNames.Add(gene.def.defName);
}
// Извлекаем гены цвета волос
var hairColorGenes = pawn
.genes.GenesListForReading
.Where(gene => gene.Active && IsHairColorGene(gene.def) && !IsGeneSuppressed(pawn, gene))
.ToList();
foreach (var gene in hairColorGenes)
{
data.HairColorGeneDefNames.Add(gene.def.defName);
}
}
// Извлекаем черты характера // Извлекаем черты характера
if (pawn.story.traits?.allTraits != null) if (pawn.story.traits?.allTraits != null)
{ {
@@ -85,12 +111,10 @@ namespace AIImages.Services
); );
} }
// Цвет кожи (с умным описанием) // Цвет кожи (с умным описанием, сначала проверяем гены)
if (pawn.story.SkinColor != null) string skinDescription = ColorDescriptionService.GetSkinToneDescription(pawn);
if (!string.IsNullOrEmpty(skinDescription))
{ {
string skinDescription = ColorDescriptionService.GetSkinToneDescription(
pawn.story.SkinColor
);
sb.AppendLine("AIImages.Appearance.SkinTone".Translate(skinDescription)); sb.AppendLine("AIImages.Appearance.SkinTone".Translate(skinDescription));
} }
@@ -98,11 +122,10 @@ namespace AIImages.Services
if (pawn.story.hairDef != null) if (pawn.story.hairDef != null)
{ {
sb.AppendLine("AIImages.Appearance.Hairstyle".Translate(pawn.story.hairDef.label)); sb.AppendLine("AIImages.Appearance.Hairstyle".Translate(pawn.story.hairDef.label));
if (pawn.story.HairColor != null) // Используем метод с проверкой генов
string hairColorDescription = ColorDescriptionService.GetHairColorDescription(pawn);
if (!string.IsNullOrEmpty(hairColorDescription))
{ {
string hairColorDescription = ColorDescriptionService.GetHairColorDescription(
pawn.story.HairColor
);
sb.AppendLine( sb.AppendLine(
"AIImages.Appearance.HairColorDesc".Translate(hairColorDescription) "AIImages.Appearance.HairColorDesc".Translate(hairColorDescription)
); );
@@ -182,5 +205,114 @@ namespace AIImages.Services
sb.AppendLine(); sb.AppendLine();
} }
/// <summary>
/// Проверяет, является ли ген геном цвета кожи
/// </summary>
private bool IsSkinColorGene(GeneDef geneDef)
{
if (geneDef == null)
return false;
string geneDefName = geneDef.defName;
return geneDefName.StartsWith("Skin_")
|| geneDefName.Contains("SkinColor")
|| geneDefName.Contains("SkinTone");
}
/// <summary>
/// Проверяет, является ли ген геном цвета волос
/// </summary>
private bool IsHairColorGene(GeneDef geneDef)
{
if (geneDef == null)
return false;
string geneDefName = geneDef.defName;
return geneDefName.StartsWith("Hair_")
|| geneDefName.Contains("HairColor")
|| geneDefName.Contains("HairColour")
|| geneDefName.Contains("HairTone")
|| (geneDefName.Contains("Hair") && (
geneDefName.Contains("Color")
|| geneDefName.Contains("Colour")
|| geneDefName.Contains("Red")
|| geneDefName.Contains("Blue")
|| geneDefName.Contains("Green")
|| geneDefName.Contains("Blond")
|| geneDefName.Contains("Brown")
|| geneDefName.Contains("Black")
|| geneDefName.Contains("White")
|| geneDefName.Contains("Light")
|| geneDefName.Contains("Dark")
|| geneDefName.Contains("Ginger")
|| geneDefName.Contains("Auburn")
|| geneDefName.Contains("Copper")
|| geneDefName.Contains("Orange")
));
}
/// <summary>
/// Проверяет, подавлен ли ген другим активным геном
/// </summary>
private bool IsGeneSuppressed(Pawn pawn, Gene gene)
{
if (pawn?.genes == null || gene?.def == null)
return false;
// В RimWorld, если ген активен, он уже учтён в системе
// Но мы можем проверить, подавляет ли другой активный ген данный ген
try
{
// Проверяем все активные гены на предмет подавления данного гена
foreach (var otherGene in pawn.genes.GenesListForReading)
{
if (otherGene.Active && otherGene != gene && otherGene.def != null)
{
// Проверяем через reflection, так как свойство suppresses может быть приватным
var suppressesField = otherGene.def.GetType().GetField("suppresses");
if (suppressesField != null)
{
var suppresses = suppressesField.GetValue(otherGene.def);
if (suppresses is System.Collections.Generic.List<GeneDef> suppressesList)
{
if (suppressesList.Contains(gene.def))
return true;
}
else if (suppresses is System.Collections.Generic.IEnumerable<GeneDef> suppressesEnum)
{
if (suppressesEnum.Contains(gene.def))
return true;
}
}
// Также проверяем свойство, если оно публичное
var suppressesProperty = otherGene.def.GetType().GetProperty("suppresses");
if (suppressesProperty != null)
{
var suppresses = suppressesProperty.GetValue(otherGene.def);
if (suppresses is System.Collections.Generic.List<GeneDef> suppressesList)
{
if (suppressesList.Contains(gene.def))
return true;
}
else if (suppresses is System.Collections.Generic.IEnumerable<GeneDef> suppressesEnum)
{
if (suppressesEnum.Contains(gene.def))
return true;
}
}
}
}
}
catch
{
// Если reflection не работает, возвращаем false
// В RimWorld свойство Active уже учитывает подавление
return false;
}
return false;
}
} }
} }

View File

@@ -0,0 +1,83 @@
using System;
using AIImages.Settings;
using Verse;
namespace AIImages.Services
{
/// <summary>
/// Простой DI контейнер для управления зависимостями мода
/// Используется вместо статического Service Locator паттерна
/// </summary>
public class ServiceContainer : IDisposable
{
private bool _disposed;
// Сервисы
public IPawnDescriptionService PawnDescriptionService { get; }
public IPromptGeneratorService PromptGeneratorService { get; }
public IStableDiffusionApiService ApiService { get; private set; }
// Настройки
public AIImagesModSettings Settings { get; }
public ServiceContainer(AIImagesModSettings settings)
{
if (settings == null)
{
throw new ArgumentNullException(nameof(settings));
}
Settings = settings;
// Инициализируем сервисы с внедрением зависимостей
PawnDescriptionService = new PawnDescriptionService();
PromptGeneratorService = new AdvancedPromptGenerator();
// Создаем API сервис с текущими настройками
RecreateApiService();
Log.Message("[AI Images] ServiceContainer initialized successfully");
}
/// <summary>
/// Пересоздает API сервис с новыми настройками (например, когда изменился endpoint)
/// </summary>
public void RecreateApiService()
{
// Освобождаем старый сервис, если он был
if (ApiService is IDisposable disposable)
{
disposable.Dispose();
}
// Создаем новый с актуальными настройками
ApiService = new StableDiffusionNetAdapter(Settings.apiEndpoint, Settings.savePath);
Log.Message($"[AI Images] API service recreated with endpoint: {Settings.apiEndpoint}");
}
/// <summary>
/// Проверяет, нужно ли пересоздать API сервис (например, если изменился endpoint)
/// </summary>
public bool ShouldRecreateApiService(string newEndpoint)
{
// Можно добавить более сложную логику проверки
return Settings.apiEndpoint != newEndpoint;
}
public void Dispose()
{
if (_disposed)
return;
// Освобождаем ресурсы API сервиса
if (ApiService is IDisposable disposable)
{
disposable.Dispose();
}
_disposed = true;
Log.Message("[AI Images] ServiceContainer disposed");
}
}
}

View File

@@ -1,285 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using AIImages.Models;
using Newtonsoft.Json;
using Verse;
namespace AIImages.Services
{
/// <summary>
/// Сервис для работы с Stable Diffusion API (AUTOMATIC1111 WebUI)
/// </summary>
public class StableDiffusionApiService : IStableDiffusionApiService
{
private readonly HttpClient httpClient;
private readonly string saveFolderPath;
public StableDiffusionApiService(string savePath = "AIImages/Generated")
{
httpClient = new HttpClient { Timeout = TimeSpan.FromMinutes(5) };
// Определяем путь для сохранения
saveFolderPath = Path.Combine(GenFilePaths.SaveDataFolderPath, savePath);
// Создаем папку, если не существует
if (!Directory.Exists(saveFolderPath))
{
Directory.CreateDirectory(saveFolderPath);
}
}
public async Task<GenerationResult> GenerateImageAsync(GenerationRequest request)
{
try
{
Log.Message(
$"[AI Images] Starting image generation with prompt: {request.Prompt.Substring(0, Math.Min(50, request.Prompt.Length))}..."
);
// Формируем JSON запрос для AUTOMATIC1111 API
var apiRequest = new
{
prompt = request.Prompt,
negative_prompt = request.NegativePrompt,
steps = request.Steps,
cfg_scale = request.CfgScale,
width = request.Width,
height = request.Height,
sampler_name = request.Sampler,
scheduler = request.Scheduler,
seed = request.Seed,
save_images = false,
send_images = true,
};
string jsonRequest = JsonConvert.SerializeObject(apiRequest);
var content = new StringContent(jsonRequest, Encoding.UTF8, "application/json");
// Отправляем запрос
string endpoint = $"{request.Model}/sdapi/v1/txt2img";
HttpResponseMessage response = await httpClient.PostAsync(endpoint, content);
if (!response.IsSuccessStatusCode)
{
string errorContent = await response.Content.ReadAsStringAsync();
Log.Error(
$"[AI Images] API request failed: {response.StatusCode} - {errorContent}"
);
return GenerationResult.Failure($"API Error: {response.StatusCode}");
}
string jsonResponse = await response.Content.ReadAsStringAsync();
var apiResponse = JsonConvert.DeserializeObject<Txt2ImgResponse>(jsonResponse);
if (apiResponse?.images == null || apiResponse.images.Length == 0)
{
return GenerationResult.Failure("No images returned from API");
}
// Декодируем изображение из base64
byte[] imageData = Convert.FromBase64String(apiResponse.images[0]);
// Сохраняем изображение
string fileName = $"pawn_{DateTime.Now:yyyyMMdd_HHmmss}.png";
string fullPath = Path.Combine(saveFolderPath, fileName);
await File.WriteAllBytesAsync(fullPath, imageData);
Log.Message($"[AI Images] Image generated successfully and saved to: {fullPath}");
return GenerationResult.SuccessResult(imageData, fullPath, request);
}
catch (TaskCanceledException)
{
return GenerationResult.Failure("Request timeout. Generation took too long.");
}
catch (HttpRequestException ex)
{
Log.Error($"[AI Images] HTTP error: {ex.Message}");
return GenerationResult.Failure($"Connection error: {ex.Message}");
}
catch (Exception ex)
{
Log.Error($"[AI Images] Unexpected error: {ex.Message}\n{ex.StackTrace}");
return GenerationResult.Failure($"Error: {ex.Message}");
}
}
public async Task<bool> CheckApiAvailability(string apiEndpoint)
{
try
{
string endpoint = $"{apiEndpoint}/sdapi/v1/sd-models";
HttpResponseMessage response = await httpClient.GetAsync(endpoint);
return response.IsSuccessStatusCode;
}
catch (Exception ex)
{
Log.Warning($"[AI Images] API check failed: {ex.Message}");
return false;
}
}
public async Task<List<string>> GetAvailableModels(string apiEndpoint)
{
try
{
string endpoint = $"{apiEndpoint}/sdapi/v1/sd-models";
HttpResponseMessage response = await httpClient.GetAsync(endpoint);
if (!response.IsSuccessStatusCode)
return new List<string>();
string jsonResponse = await response.Content.ReadAsStringAsync();
var models = JsonConvert.DeserializeObject<List<SdModel>>(jsonResponse);
var modelNames = new List<string>();
if (models != null)
{
foreach (var model in models)
{
modelNames.Add(model.title ?? model.model_name);
}
}
Log.Message($"[AI Images] Found {modelNames.Count} models");
return modelNames;
}
catch (Exception ex)
{
Log.Error($"[AI Images] Failed to load models: {ex.Message}");
return new List<string>();
}
}
public async Task<List<string>> GetAvailableSamplers(string apiEndpoint)
{
try
{
string endpoint = $"{apiEndpoint}/sdapi/v1/samplers";
HttpResponseMessage response = await httpClient.GetAsync(endpoint);
if (!response.IsSuccessStatusCode)
return GetDefaultSamplers();
string jsonResponse = await response.Content.ReadAsStringAsync();
var samplers = JsonConvert.DeserializeObject<List<SdSampler>>(jsonResponse);
var samplerNames = new List<string>();
if (samplers != null)
{
foreach (var sampler in samplers)
{
samplerNames.Add(sampler.name);
}
}
Log.Message($"[AI Images] Found {samplerNames.Count} samplers");
return samplerNames;
}
catch (Exception ex)
{
Log.Warning($"[AI Images] Failed to load samplers: {ex.Message}");
return GetDefaultSamplers();
}
}
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>
{
"Euler a",
"Euler",
"LMS",
"Heun",
"DPM2",
"DPM2 a",
"DPM++ 2S a",
"DPM++ 2M",
"DPM++ SDE",
"DPM fast",
"DPM adaptive",
"LMS Karras",
"DPM2 Karras",
"DPM2 a Karras",
"DPM++ 2S a Karras",
"DPM++ 2M Karras",
"DPM++ SDE Karras",
"DDIM",
"PLMS",
};
}
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
{
public string[] images { get; set; }
}
private sealed class SdModel
{
public string title { get; set; }
public string model_name { get; set; }
}
private sealed class SdSampler
{
public string name { get; set; }
}
private sealed class SdScheduler
{
public string name { get; set; }
}
#pragma warning restore S3459, S1144
}
}

View File

@@ -0,0 +1,352 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using AIImages.Models;
using StableDiffusionNet;
using StableDiffusionNet.Exceptions;
using StableDiffusionNet.Interfaces;
using StableDiffusionNet.Models.Requests;
using Verse;
namespace AIImages.Services
{
/// <summary>
/// Адаптер для Stable Diffusion API (AUTOMATIC1111 WebUI)
/// Использует библиотеку StableDiffusionNet для работы с API
/// </summary>
public class StableDiffusionNetAdapter : IStableDiffusionApiService, IDisposable
{
private readonly IStableDiffusionClient _client;
private readonly string _saveFolderPath;
private bool _disposed;
public StableDiffusionNetAdapter(string apiEndpoint, string savePath = "AIImages/Generated")
{
if (string.IsNullOrEmpty(apiEndpoint))
{
throw new ArgumentException(
"API endpoint cannot be null or empty",
nameof(apiEndpoint)
);
}
// Определяем путь для сохранения
_saveFolderPath = Path.Combine(GenFilePaths.SaveDataFolderPath, savePath);
// Создаем папку, если не существует
if (!Directory.Exists(_saveFolderPath))
{
Directory.CreateDirectory(_saveFolderPath);
}
// Создаем клиент StableDiffusion используя Builder
_client = new StableDiffusionClientBuilder()
.WithBaseUrl(apiEndpoint)
.WithTimeout(300) // 5 минут в секундах
.WithRetry(retryCount: 3, retryDelayMilliseconds: 1000)
.Build();
Log.Message(
$"[AI Images] StableDiffusion adapter initialized with endpoint: {apiEndpoint}"
);
}
public async Task<GenerationResult> GenerateImageAsync(
GenerationRequest request,
CancellationToken cancellationToken = default
)
{
ThrowIfDisposed();
if (request == null)
{
return GenerationResult.Failure("Request cannot be null");
}
try
{
Log.Message(
$"[AI Images] Starting image generation with prompt: {request.Prompt.Substring(0, Math.Min(50, request.Prompt.Length))}..."
);
// Маппируем наш запрос на запрос библиотеки StableDiffusionNet
var sdRequest = new TextToImageRequest
{
Prompt = request.Prompt,
NegativePrompt = request.NegativePrompt,
Steps = request.Steps,
CfgScale = request.CfgScale,
Width = request.Width,
Height = request.Height,
SamplerName = request.Sampler,
Scheduler = request.Scheduler,
Seed = request.Seed,
SaveImages = request.SaveImagesToServer, // Сохранять ли изображения на сервере
};
// Выполняем запрос через библиотеку (с встроенной retry логикой)
var response = await _client.TextToImage.GenerateAsync(
sdRequest,
cancellationToken
);
if (response?.Images == null || response.Images.Count == 0)
{
return GenerationResult.Failure("No images returned from API");
}
// Декодируем изображение из base64
byte[] imageData = Convert.FromBase64String(response.Images[0]);
// Сохраняем изображение
string fileName = $"pawn_{DateTime.Now:yyyyMMdd_HHmmss}.png";
string fullPath = Path.Combine(_saveFolderPath, fileName);
await File.WriteAllBytesAsync(fullPath, imageData);
Log.Message($"[AI Images] Image generated successfully and saved to: {fullPath}");
return GenerationResult.SuccessResult(imageData, fullPath, request);
}
catch (TaskCanceledException)
{
Log.Warning("[AI Images] Request timeout. Generation took too long.");
return GenerationResult.Failure("Request timeout. Generation took too long.");
}
catch (OperationCanceledException)
{
Log.Message("[AI Images] Image generation was cancelled");
return GenerationResult.Failure("Generation cancelled");
}
catch (HttpRequestException ex)
{
Log.Error($"[AI Images] HTTP error: {ex.Message}");
return GenerationResult.Failure($"Connection error: {ex.Message}");
}
catch (StableDiffusionException ex)
{
Log.Error($"[AI Images] StableDiffusion API error: {ex.Message}");
return GenerationResult.Failure($"API Error: {ex.Message}");
}
catch (Exception ex)
{
Log.Error($"[AI Images] Unexpected error: {ex.Message}\n{ex.StackTrace}");
return GenerationResult.Failure($"Error: {ex.Message}");
}
}
public async Task<GenerationProgress> GetProgressAsync(
CancellationToken cancellationToken = default
)
{
ThrowIfDisposed();
try
{
// Используем Progress сервис библиотеки
var progress = await _client.Progress.GetProgressAsync(cancellationToken);
// Маппируем на наш тип
return new GenerationProgress
{
Progress = progress.Progress,
CurrentStep = progress.State?.SamplingStep ?? 0,
TotalSteps = progress.State?.SamplingSteps ?? 0,
EtaRelative = progress.EtaRelative,
IsActive = progress.Progress > 0 && progress.Progress < 1.0,
CurrentImage = progress.CurrentImage, // Превью текущего состояния генерации
};
}
catch (Exception ex)
{
Log.Warning($"[AI Images] Failed to get progress: {ex.Message}");
// Возвращаем пустой прогресс при ошибке
return new GenerationProgress
{
Progress = 0,
CurrentStep = 0,
TotalSteps = 0,
EtaRelative = 0,
IsActive = false,
};
}
}
public async Task<bool> CheckApiAvailability(
string apiEndpoint,
CancellationToken cancellationToken = default
)
{
ThrowIfDisposed();
try
{
// Используем встроенный метод HealthCheckAsync библиотеки
var healthCheck = await _client.HealthCheckAsync(cancellationToken);
return healthCheck.IsHealthy;
}
catch (Exception ex)
{
Log.Warning($"[AI Images] API check failed: {ex.Message}");
return false;
}
}
public async Task<List<string>> GetAvailableModels(
string apiEndpoint,
CancellationToken cancellationToken = default
)
{
ThrowIfDisposed();
try
{
// Используем Models сервис библиотеки
var models = await _client.Models.GetModelsAsync(cancellationToken);
var modelNames = new List<string>();
if (models != null)
{
foreach (var model in models)
{
// Используем Title или ModelName в зависимости от того, что доступно
modelNames.Add(model.Title ?? model.ModelName);
}
}
Log.Message($"[AI Images] Found {modelNames.Count} models");
return modelNames;
}
catch (Exception ex)
{
Log.Error($"[AI Images] Failed to load models: {ex.Message}");
return new List<string>();
}
}
public async Task<List<string>> GetAvailableSamplers(
string apiEndpoint,
CancellationToken cancellationToken = default
)
{
ThrowIfDisposed();
try
{
// Используем Samplers сервис библиотеки
var samplers = await _client.Samplers.GetSamplersAsync(cancellationToken);
var samplerNames = new List<string>();
if (samplers != null)
{
foreach (var sampler in samplers)
{
samplerNames.Add(sampler.Name);
}
}
Log.Message($"[AI Images] Found {samplerNames.Count} samplers");
return samplerNames;
}
catch (Exception ex)
{
Log.Warning($"[AI Images] Failed to load samplers: {ex.Message}");
return GetDefaultSamplers();
}
}
public async Task<List<string>> GetAvailableSchedulers(
string apiEndpoint,
CancellationToken cancellationToken = default
)
{
ThrowIfDisposed();
try
{
// Используем Schedulers сервис библиотеки (доступен с версии 1.1.1)
var schedulers = await _client.Schedulers.GetSchedulersAsync(cancellationToken);
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>
{
"Euler a",
"Euler",
"LMS",
"Heun",
"DPM2",
"DPM2 a",
"DPM++ 2S a",
"DPM++ 2M",
"DPM++ SDE",
"DPM fast",
"DPM adaptive",
"LMS Karras",
"DPM2 Karras",
"DPM2 a Karras",
"DPM++ 2S a Karras",
"DPM++ 2M Karras",
"DPM++ SDE Karras",
"DDIM",
"PLMS",
};
}
private List<string> GetDefaultSchedulers()
{
return new List<string>
{
"Automatic",
"Uniform",
"Karras",
"Exponential",
"Polyexponential",
"SGM Uniform",
};
}
private void ThrowIfDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(StableDiffusionNetAdapter));
}
}
public void Dispose()
{
if (_disposed)
return;
// Dispose клиента StableDiffusion
if (_client is IDisposable disposableClient)
{
disposableClient.Dispose();
}
_disposed = true;
}
}
}

View File

@@ -14,7 +14,7 @@ namespace AIImages.Settings
public string apiEndpoint = "http://127.0.0.1:7860"; public string apiEndpoint = "http://127.0.0.1:7860";
public string selectedModel = ""; public string selectedModel = "";
public string selectedSampler = "Euler a"; public string selectedSampler = "Euler a";
public string selectedScheduler = "Automatic"; public string selectedScheduler = "Automatic"; // С большой буквы для API
// Кэшированные списки из API (не сохраняются) // Кэшированные списки из API (не сохраняются)
[Unsaved] [Unsaved]
@@ -38,16 +38,21 @@ namespace AIImages.Settings
public string baseNegativePrompt = public string baseNegativePrompt =
"ugly, deformed, low quality, blurry, bad anatomy, worst quality"; "ugly, deformed, low quality, blurry, bad anatomy, worst quality";
// Художественный стиль // Художественный стиль (defName)
public ArtStyle artStyle = ArtStyle.Realistic; public string artStyleDefName = "ArtStyle_Realistic";
// Путь для сохранения // Путь для сохранения
public string savePath = "AIImages/Generated"; public string savePath = "AIImages/Generated";
// Тип генерации
public AIImages.Models.ImageType imageType = AIImages.Models.ImageType.Portrait;
// Флаги // Флаги
public bool autoLoadModels = true; public bool autoLoadModels = true;
public bool showTechnicalInfo = true; public bool showTechnicalInfo = true;
public bool saveGenerationHistory = true; public bool saveGenerationHistory = true;
public bool saveImagesToServer = false;
public bool enableDebugLogs = false;
public override void ExposeData() public override void ExposeData()
{ {
@@ -69,13 +74,17 @@ namespace AIImages.Settings
"ugly, deformed, low quality, blurry, bad anatomy, worst quality" "ugly, deformed, low quality, blurry, bad anatomy, worst quality"
); );
Scribe_Values.Look(ref artStyle, "artStyle", ArtStyle.Realistic); Scribe_Values.Look(ref artStyleDefName, "artStyleDefName", "ArtStyle_Realistic");
Scribe_Values.Look(ref savePath, "savePath", "AIImages/Generated"); Scribe_Values.Look(ref savePath, "savePath", "AIImages/Generated");
Scribe_Values.Look(ref imageType, "imageType", AIImages.Models.ImageType.Portrait);
Scribe_Values.Look(ref autoLoadModels, "autoLoadModels", true); Scribe_Values.Look(ref autoLoadModels, "autoLoadModels", true);
Scribe_Values.Look(ref showTechnicalInfo, "showTechnicalInfo", true); Scribe_Values.Look(ref showTechnicalInfo, "showTechnicalInfo", true);
Scribe_Values.Look(ref saveGenerationHistory, "saveGenerationHistory", true); Scribe_Values.Look(ref saveGenerationHistory, "saveGenerationHistory", true);
Scribe_Values.Look(ref saveImagesToServer, "saveImagesToServer", false);
Scribe_Values.Look(ref enableDebugLogs, "enableDebugLogs", false);
base.ExposeData(); base.ExposeData();
} }
@@ -95,9 +104,10 @@ namespace AIImages.Settings
Scheduler = selectedScheduler, Scheduler = selectedScheduler,
Seed = seed, Seed = seed,
Model = selectedModel, Model = selectedModel,
ArtStyle = artStyle, ArtStyleDefName = artStyleDefName,
PositivePrompt = basePositivePrompt, PositivePrompt = basePositivePrompt,
NegativePrompt = baseNegativePrompt, NegativePrompt = baseNegativePrompt,
ImageType = imageType,
}; };
} }
} }

View File

@@ -1,7 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using AIImages.Helpers;
using AIImages.Models; using AIImages.Models;
using AIImages.Services;
using AIImages.Settings; using AIImages.Settings;
using RimWorld; using RimWorld;
using UnityEngine; using UnityEngine;
@@ -19,17 +21,21 @@ namespace AIImages
private static string widthBuffer; private static string widthBuffer;
private static string heightBuffer; private static string heightBuffer;
public static void DoSettingsWindowContents(Rect inRect, AIImagesModSettings settings) public static void DoSettingsWindowContents(
Rect inRect,
AIImagesModSettings settings,
ServiceContainer serviceContainer
)
{ {
InitializeBuffers(settings); InitializeBuffers(settings);
Listing_Standard listingStandard = new Listing_Standard(); Listing_Standard listingStandard = new Listing_Standard();
Rect viewRect = new Rect(0f, 0f, inRect.width - 20f, 1200f); Rect viewRect = new Rect(0f, 0f, inRect.width - 20f, 1250f);
Widgets.BeginScrollView(inRect, ref scrollPosition, viewRect); Widgets.BeginScrollView(inRect, ref scrollPosition, viewRect);
listingStandard.Begin(viewRect); listingStandard.Begin(viewRect);
DrawApiSettings(listingStandard, settings); DrawApiSettings(listingStandard, settings, serviceContainer);
DrawGenerationSettings(listingStandard, settings); DrawGenerationSettings(listingStandard, settings);
DrawSamplerSchedulerSettings(listingStandard, settings); DrawSamplerSchedulerSettings(listingStandard, settings);
DrawPromptsSettings(listingStandard, settings); DrawPromptsSettings(listingStandard, settings);
@@ -51,7 +57,8 @@ namespace AIImages
private static void DrawApiSettings( private static void DrawApiSettings(
Listing_Standard listingStandard, Listing_Standard listingStandard,
AIImagesModSettings settings AIImagesModSettings settings,
ServiceContainer serviceContainer
) )
{ {
listingStandard.Label( listingStandard.Label(
@@ -61,27 +68,40 @@ namespace AIImages
); );
listingStandard.GapLine(); listingStandard.GapLine();
string oldEndpoint = settings.apiEndpoint;
listingStandard.Label("AIImages.Settings.ApiEndpoint".Translate() + ":"); listingStandard.Label("AIImages.Settings.ApiEndpoint".Translate() + ":");
settings.apiEndpoint = listingStandard.TextEntry(settings.apiEndpoint); settings.apiEndpoint = listingStandard.TextEntry(settings.apiEndpoint);
listingStandard.Gap(8f); listingStandard.Gap(8f);
if (listingStandard.ButtonText("AIImages.Settings.TestConnection".Translate())) // Если endpoint изменился, пересоздаем API сервис
if (oldEndpoint != settings.apiEndpoint)
{ {
_ = TestApiConnection(settings.apiEndpoint); try
{
serviceContainer.RecreateApiService();
}
catch (Exception ex)
{
Log.Error($"[AI Images] Failed to recreate API service: {ex.Message}");
Messages.Message(
"Failed to update API endpoint. Check the log.",
MessageTypeDefOf.RejectInput
);
}
} }
if (listingStandard.ButtonText("AIImages.Settings.LoadModels".Translate())) if (listingStandard.ButtonText("AIImages.Settings.TestConnection".Translate()))
{ {
_ = LoadModelsFromApi(settings); TestApiConnection(serviceContainer.ApiService, settings.apiEndpoint);
}
if (listingStandard.ButtonText("AIImages.Settings.LoadFromApi".Translate()))
{
LoadAllFromApi(serviceContainer.ApiService, settings);
} }
DrawModelDropdown(listingStandard, settings); DrawModelDropdown(listingStandard, settings);
if (listingStandard.ButtonText("AIImages.Settings.LoadSamplersSchedulers".Translate()))
{
_ = LoadSamplersAndSchedulers(settings);
}
listingStandard.Gap(12f); listingStandard.Gap(12f);
} }
@@ -154,21 +174,37 @@ namespace AIImages
AIImagesModSettings settings AIImagesModSettings settings
) )
{ {
// Получаем текущий стиль
var currentStyleDef = DefDatabase<ArtStyleDef>.GetNamedSilentFail(
settings.artStyleDefName
);
string currentStyleLabel = currentStyleDef?.label ?? settings.artStyleDefName;
if ( if (
listingStandard.ButtonTextLabeled( listingStandard.ButtonTextLabeled(
"AIImages.Settings.ArtStyle".Translate(), "AIImages.Settings.ArtStyle".Translate(),
settings.artStyle.ToString() currentStyleLabel
) )
) )
{ {
List<FloatMenuOption> styleOptions = new List<FloatMenuOption>(); List<FloatMenuOption> styleOptions = new List<FloatMenuOption>();
foreach (ArtStyle style in Enum.GetValues(typeof(ArtStyle)))
// Получаем все стили из DefDatabase и сортируем по sortOrder
var allStyles = DefDatabase<ArtStyleDef>.AllDefs.OrderBy(s => s.sortOrder);
foreach (var styleDef in allStyles)
{ {
ArtStyle localStyle = style; string localDefName = styleDef.defName;
string localLabel = styleDef.label;
styleOptions.Add( styleOptions.Add(
new FloatMenuOption(style.ToString(), () => settings.artStyle = localStyle) new FloatMenuOption(
localLabel,
() => settings.artStyleDefName = localDefName
)
); );
} }
Find.WindowStack.Add(new FloatMenu(styleOptions)); Find.WindowStack.Add(new FloatMenu(styleOptions));
} }
} }
@@ -200,34 +236,47 @@ namespace AIImages
AIImagesModSettings settings AIImagesModSettings settings
) )
{ {
listingStandard.Gap(4f); // Получаем все предустановки размеров из DefDatabase
Rect presetRect1 = listingStandard.GetRect(30f); var allPresets = DefDatabase<ImageSizePresetDef>
DrawPresetButton(presetRect1, 0f, "512x512", 512, 512, settings); .AllDefs.OrderBy(p => p.sortOrder)
DrawPresetButton(presetRect1, 85f, "512x768", 512, 768, settings); .ToList();
DrawPresetButton(presetRect1, 170f, "768x768", 768, 768, settings);
listingStandard.Gap(4f); if (!allPresets.Any())
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 = width; return;
settings.height = height; }
widthBuffer = width.ToString();
heightBuffer = height.ToString(); listingStandard.Gap(4f);
// Разбиваем на строки по 3 кнопки
int buttonsPerRow = 3;
float buttonWidth = 80f;
float spacing = 5f;
for (int i = 0; i < allPresets.Count; i += buttonsPerRow)
{
Rect rowRect = listingStandard.GetRect(30f);
for (int j = 0; j < buttonsPerRow && (i + j) < allPresets.Count; j++)
{
var preset = allPresets[i + j];
float xOffset = j * (buttonWidth + spacing);
if (
Widgets.ButtonText(
new Rect(rowRect.x + xOffset, rowRect.y, buttonWidth, 30f),
preset.label
)
)
{
settings.width = preset.width;
settings.height = preset.height;
widthBuffer = preset.width.ToString();
heightBuffer = preset.height.ToString();
}
}
listingStandard.Gap(4f);
} }
} }
@@ -341,152 +390,156 @@ namespace AIImages
"AIImages.Settings.SaveHistory".Translate(), "AIImages.Settings.SaveHistory".Translate(),
ref settings.saveGenerationHistory ref settings.saveGenerationHistory
); );
listingStandard.CheckboxLabeled(
"AIImages.Settings.SaveImagesToServer".Translate(),
ref settings.saveImagesToServer,
"AIImages.Settings.SaveImagesToServerTooltip".Translate()
);
listingStandard.CheckboxLabeled(
"AIImages.Settings.EnableDebugLogs".Translate(),
ref settings.enableDebugLogs,
"AIImages.Settings.EnableDebugLogsTooltip".Translate()
);
listingStandard.Gap(12f); listingStandard.Gap(12f);
listingStandard.Label("AIImages.Settings.SavePath".Translate() + ":"); listingStandard.Label("AIImages.Settings.SavePath".Translate() + ":");
settings.savePath = listingStandard.TextEntry(settings.savePath); settings.savePath = listingStandard.TextEntry(settings.savePath);
listingStandard.Gap(12f);
// Кнопка очистки всех изображений
if (listingStandard.ButtonText("AIImages.Settings.ClearAllImages".Translate()))
{
ClearAllGeneratedImages(settings);
}
} }
private static async System.Threading.Tasks.Task TestApiConnection(string endpoint) private static void TestApiConnection(
IStableDiffusionApiService apiService,
string endpoint
)
{ {
try _ = AsyncHelper.RunAsync(
{ async () =>
Log.Message($"[AI Images] Testing connection to {endpoint}..."); {
bool available = await AIImagesMod.ApiService.CheckApiAvailability(endpoint); Log.Message($"[AI Images] Testing connection to {endpoint}...");
bool available = await apiService.CheckApiAvailability(endpoint);
if (available) if (available)
{ {
Messages.Message( Messages.Message(
"AIImages.Settings.ConnectionSuccess".Translate(), "AIImages.Settings.ConnectionSuccess".Translate(),
MessageTypeDefOf.PositiveEvent MessageTypeDefOf.PositiveEvent
); );
} }
else else
{ {
Messages.Message( Messages.Message(
"AIImages.Settings.ConnectionFailed".Translate(), "AIImages.Settings.ConnectionFailed".Translate(),
MessageTypeDefOf.RejectInput MessageTypeDefOf.RejectInput
); );
} }
} },
catch (Exception ex) "API Connection Test"
{ );
Messages.Message($"Error: {ex.Message}", MessageTypeDefOf.RejectInput);
}
} }
private static async System.Threading.Tasks.Task LoadModelsFromApi( private static void LoadAllFromApi(
IStableDiffusionApiService apiService,
AIImagesModSettings settings AIImagesModSettings settings
) )
{ {
try _ = AsyncHelper.RunAsync(
{ async () =>
Log.Message("[AI Images] Loading models from API...");
var models = await AIImagesMod.ApiService.GetAvailableModels(settings.apiEndpoint);
settings.availableModels = models;
if (models.Count > 0)
{ {
Messages.Message( Log.Message("[AI Images] Loading models, samplers and schedulers from API...");
"AIImages.Settings.ModelsLoaded".Translate(models.Count),
MessageTypeDefOf.PositiveEvent
);
if ( // Загружаем модели
( var models = await apiService.GetAvailableModels(settings.apiEndpoint);
string.IsNullOrEmpty(settings.selectedModel) settings.availableModels = models;
|| !models.Contains(settings.selectedModel)
) // Загружаем семплеры
&& models.Count > 0 var samplers = await apiService.GetAvailableSamplers(settings.apiEndpoint);
) settings.availableSamplers = samplers;
// Загружаем schedulers
var schedulers = await apiService.GetAvailableSchedulers(settings.apiEndpoint);
settings.availableSchedulers = schedulers;
int totalCount = models.Count + samplers.Count + schedulers.Count;
if (totalCount > 0)
{ {
settings.selectedModel = models[0]; ShowSuccessMessage(models.Count, samplers.Count, schedulers.Count);
AutoSelectDefaults(settings, models, samplers, schedulers);
} }
} else
else {
{ Messages.Message(
Messages.Message( "AIImages.Settings.NothingLoaded".Translate(),
"AIImages.Settings.NoModelsFound".Translate(), MessageTypeDefOf.RejectInput
MessageTypeDefOf.RejectInput );
); }
} },
} "Load API Data"
catch (Exception ex) );
}
private static void ShowSuccessMessage(int modelCount, int samplerCount, int schedulerCount)
{
Messages.Message(
"AIImages.Settings.AllLoaded".Translate(modelCount, samplerCount, schedulerCount),
MessageTypeDefOf.PositiveEvent
);
}
private static void AutoSelectDefaults(
AIImagesModSettings settings,
List<string> models,
List<string> samplers,
List<string> schedulers
)
{
AutoSelectIfNeeded(ref settings.selectedModel, models, settings.selectedModel);
AutoSelectIfNeeded(ref settings.selectedSampler, samplers, settings.selectedSampler);
AutoSelectIfNeeded(
ref settings.selectedScheduler,
schedulers,
settings.selectedScheduler
);
}
private static void AutoSelectIfNeeded(
ref string selectedValue,
List<string> availableValues,
string currentValue
)
{
bool needsSelection =
string.IsNullOrEmpty(currentValue) || !availableValues.Contains(currentValue);
if (needsSelection && availableValues.Count > 0)
{ {
Messages.Message( selectedValue = availableValues[0];
$"Error loading models: {ex.Message}",
MessageTypeDefOf.RejectInput
);
} }
} }
private static async System.Threading.Tasks.Task LoadSamplersAndSchedulers( private static void ClearAllGeneratedImages(AIImagesModSettings settings)
AIImagesModSettings settings
)
{ {
try Find.WindowStack.Add(
{ Dialog_MessageBox.CreateConfirmation(
Log.Message("[AI Images] Loading samplers and schedulers from API..."); "AIImages.Settings.ClearAllImagesConfirm".Translate(),
delegate
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]; int deletedCount = PawnPortraitHelper.ClearAllPortraits(settings.savePath);
} Messages.Message(
"AIImages.Settings.ClearAllImagesSuccess".Translate(deletedCount),
if ( MessageTypeDefOf.PositiveEvent
( );
string.IsNullOrEmpty(settings.selectedScheduler) },
|| !schedulers.Contains(settings.selectedScheduler) destructive: true
) )
&& 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
);
}
} }
} }
} }

View File

@@ -0,0 +1,287 @@
using System;
using System.Collections.Generic;
using AIImages.Settings;
namespace AIImages.Validation
{
/// <summary>
/// Валидатор для настроек мода AI Images
/// </summary>
public static class SettingsValidator
{
/// <summary>
/// Валидирует все настройки и возвращает список ошибок
/// </summary>
public static ValidationResult Validate(AIImagesModSettings settings)
{
var result = new ValidationResult();
if (settings == null)
{
result.AddError("Settings object is null");
return result;
}
// Валидация API endpoint
ValidateApiEndpoint(settings.apiEndpoint, result);
// Валидация размеров изображения
ValidateImageDimensions(settings.width, settings.height, result);
// Валидация steps
ValidateSteps(settings.steps, result);
// Валидация CFG scale
ValidateCfgScale(settings.cfgScale, result);
// Валидация sampler и scheduler
ValidateSamplerAndScheduler(
settings.selectedSampler,
settings.selectedScheduler,
result
);
// Валидация пути сохранения
ValidateSavePath(settings.savePath, result);
return result;
}
private static void ValidateApiEndpoint(string endpoint, ValidationResult result)
{
if (string.IsNullOrWhiteSpace(endpoint))
{
result.AddError("API endpoint cannot be empty");
return;
}
if (!Uri.TryCreate(endpoint, UriKind.Absolute, out Uri uri))
{
result.AddError($"Invalid API endpoint format: {endpoint}");
return;
}
if (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)
{
result.AddWarning($"API endpoint should use HTTP or HTTPS protocol: {endpoint}");
}
// Проверка на localhost/127.0.0.1
if (
uri.Host != "localhost"
&& uri.Host != "127.0.0.1"
&& !uri.Host.StartsWith("192.168.")
)
{
result.AddWarning(
"API endpoint is not pointing to a local address. Make sure the API is accessible."
);
}
}
private static void ValidateImageDimensions(int width, int height, ValidationResult result)
{
const int minDimension = 64;
const int maxDimension = 2048;
const int recommendedMin = 512;
const int recommendedMax = 1024;
if (width < minDimension || width > maxDimension)
{
result.AddError(
$"Width must be between {minDimension} and {maxDimension}. Current: {width}"
);
}
if (height < minDimension || height > maxDimension)
{
result.AddError(
$"Height must be between {minDimension} and {maxDimension}. Current: {height}"
);
}
// Проверка кратности 8 (рекомендация для Stable Diffusion)
if (width % 8 != 0)
{
result.AddWarning(
$"Width should be divisible by 8 for optimal results. Current: {width}"
);
}
if (height % 8 != 0)
{
result.AddWarning(
$"Height should be divisible by 8 for optimal results. Current: {height}"
);
}
// Предупреждения о производительности
if (width > recommendedMax || height > recommendedMax)
{
result.AddWarning(
$"Large image dimensions ({width}x{height}) may result in slow generation and high memory usage"
);
}
if (width < recommendedMin || height < recommendedMin)
{
result.AddWarning(
$"Small image dimensions ({width}x{height}) may result in lower quality"
);
}
}
private static void ValidateSteps(int steps, ValidationResult result)
{
const int minSteps = 1;
const int maxSteps = 150;
const int recommendedMin = 20;
const int recommendedMax = 50;
if (steps < minSteps || steps > maxSteps)
{
result.AddError(
$"Steps must be between {minSteps} and {maxSteps}. Current: {steps}"
);
}
if (steps < recommendedMin)
{
result.AddWarning(
$"Low steps value ({steps}) may result in lower quality. Recommended: {recommendedMin}-{recommendedMax}"
);
}
if (steps > recommendedMax)
{
result.AddWarning(
$"High steps value ({steps}) may result in slow generation with minimal quality improvement"
);
}
}
private static void ValidateCfgScale(float cfgScale, ValidationResult result)
{
const float minCfg = 1.0f;
const float maxCfg = 30.0f;
const float recommendedMin = 5.0f;
const float recommendedMax = 15.0f;
if (cfgScale < minCfg || cfgScale > maxCfg)
{
result.AddError(
$"CFG Scale must be between {minCfg} and {maxCfg}. Current: {cfgScale}"
);
}
if (cfgScale < recommendedMin)
{
result.AddWarning(
$"Low CFG scale ({cfgScale}) may ignore prompt. Recommended: {recommendedMin}-{recommendedMax}"
);
}
if (cfgScale > recommendedMax)
{
result.AddWarning(
$"High CFG scale ({cfgScale}) may result in over-saturated or distorted images"
);
}
}
private static void ValidateSamplerAndScheduler(
string sampler,
string scheduler,
ValidationResult result
)
{
if (string.IsNullOrWhiteSpace(sampler))
{
result.AddWarning("Sampler is not selected. A default sampler will be used.");
}
if (string.IsNullOrWhiteSpace(scheduler))
{
result.AddWarning("Scheduler is not selected. A default scheduler will be used.");
}
}
private static void ValidateSavePath(string savePath, ValidationResult result)
{
if (string.IsNullOrWhiteSpace(savePath))
{
result.AddError("Save path cannot be empty");
return;
}
// Проверка на недопустимые символы в пути
char[] invalidChars = System.IO.Path.GetInvalidPathChars();
if (savePath.IndexOfAny(invalidChars) >= 0)
{
result.AddError($"Save path contains invalid characters: {savePath}");
}
}
}
/// <summary>
/// Результат валидации настроек
/// </summary>
public class ValidationResult
{
private readonly List<string> _errors = new List<string>();
private readonly List<string> _warnings = new List<string>();
public bool IsValid => _errors.Count == 0;
public bool HasWarnings => _warnings.Count > 0;
public IReadOnlyList<string> Errors => _errors;
public IReadOnlyList<string> Warnings => _warnings;
public void AddError(string error)
{
if (!string.IsNullOrWhiteSpace(error))
{
_errors.Add(error);
}
}
public void AddWarning(string warning)
{
if (!string.IsNullOrWhiteSpace(warning))
{
_warnings.Add(warning);
}
}
public string GetErrorsAsString()
{
return string.Join("\n", _errors);
}
public string GetWarningsAsString()
{
return string.Join("\n", _warnings);
}
public string GetAllMessagesAsString()
{
var messages = new List<string>();
if (_errors.Count > 0)
{
messages.Add("Errors:");
messages.AddRange(_errors);
}
if (_warnings.Count > 0)
{
if (messages.Count > 0)
messages.Add("");
messages.Add("Warnings:");
messages.AddRange(_warnings);
}
return string.Join("\n", messages);
}
}
}

View File

@@ -0,0 +1,413 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AIImages.Helpers;
using RimWorld;
using UnityEngine;
using Verse;
#pragma warning disable IDE1006 // Naming Styles
namespace AIImages
{
/// <summary>
/// Окно галереи AI-сгенерированных изображений персонажа
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Style",
"IDE1006:Naming Styles",
Justification = "RimWorld Window naming convention"
)]
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Minor Code Smell",
"S101:Types should be named in PascalCase",
Justification = "RimWorld Window naming convention"
)]
public class Window_AIGallery : Window
{
private Pawn pawn;
private List<string> portraitPaths = new List<string>();
private List<Texture2D> portraitTextures = new List<Texture2D>();
private Vector2 mainScrollPosition = Vector2.zero;
private int selectedIndex = 0;
public Window_AIGallery(Pawn pawn)
{
this.pawn = pawn;
this.doCloseX = true;
this.doCloseButton = true;
this.forcePause = false;
this.absorbInputAroundWindow = false;
this.draggable = true;
this.preventCameraMotion = false;
LoadGallery();
}
public override Vector2 InitialSize => new Vector2(900f, 700f);
/// <summary>
/// Загружает галерею изображений персонажа
/// </summary>
private void LoadGallery()
{
DebugLogger.Log($"[AI Gallery] Loading gallery for {pawn?.Name}");
// Очищаем старые текстуры
UnloadTextures();
// Загружаем пути к портретам
portraitPaths = PawnPortraitHelper.GetAllPortraits(pawn);
// Загружаем текстуры
foreach (var path in portraitPaths)
{
if (File.Exists(path))
{
try
{
byte[] imageData = File.ReadAllBytes(path);
Texture2D texture = new Texture2D(2, 2);
texture.LoadImage(imageData);
portraitTextures.Add(texture);
DebugLogger.Log($"[AI Gallery] Loaded texture from: {path}");
}
catch (System.Exception ex)
{
DebugLogger.Warning($"[AI Gallery] Failed to load texture: {path}, Error: {ex.Message}");
portraitTextures.Add(null);
}
}
else
{
DebugLogger.Warning($"[AI Gallery] File not found: {path}");
portraitTextures.Add(null);
}
}
DebugLogger.Log($"[AI Gallery] Loaded {portraitTextures.Count} textures for {pawn?.Name}");
}
/// <summary>
/// Освобождает ресурсы текстур
/// </summary>
private void UnloadTextures()
{
foreach (var texture in portraitTextures)
{
if (texture != null)
{
Object.Destroy(texture);
}
}
portraitTextures.Clear();
}
/// <summary>
/// Освобождает ресурсы при закрытии окна
/// </summary>
public override void PreClose()
{
base.PreClose();
UnloadTextures();
}
/// <summary>
/// Отрисовка окна
/// </summary>
public override void DoWindowContents(Rect inRect)
{
// Заголовок
Text.Font = GameFont.Medium;
Widgets.Label(
new Rect(0f, 0f, inRect.width, 40f),
"AIImages.Gallery.Title".Translate()
);
// Имя персонажа
Text.Font = GameFont.Small;
Widgets.Label(
new Rect(0f, 40f, inRect.width, 30f),
"AIImages.Window.PawnLabel".Translate(pawn.NameShortColored.Resolve())
);
// Количество изображений
Widgets.Label(
new Rect(0f, 70f, inRect.width, 25f),
"AIImages.Gallery.Count".Translate(portraitTextures.Count)
);
// Разделитель
Widgets.DrawLineHorizontal(0f, 100f, inRect.width);
// Область для галереи
Rect galleryRect = new Rect(0f, 110f, inRect.width, inRect.height - 180f);
DrawGallery(galleryRect);
// Кнопка обновления
if (
Widgets.ButtonText(
new Rect(0f, inRect.height - 70f, inRect.width * 0.3f, 35f),
"AIImages.Window.Refresh".Translate()
)
)
{
LoadGallery();
}
// Кнопка удаления выбранного изображения
if (
portraitTextures.Count > 0
&& selectedIndex >= 0
&& selectedIndex < portraitTextures.Count
&& Widgets.ButtonText(
new Rect(inRect.width * 0.32f, inRect.height - 70f, inRect.width * 0.3f, 35f),
"AIImages.Gallery.DeleteSelected".Translate()
)
)
{
DeleteSelectedImage();
}
// Кнопка удаления всех изображений
if (
portraitTextures.Count > 0
&& Widgets.ButtonText(
new Rect(inRect.width * 0.64f, inRect.height - 70f, inRect.width * 0.36f, 35f),
"AIImages.Gallery.DeleteAll".Translate(portraitTextures.Count)
)
)
{
DeleteAllImages();
}
}
/// <summary>
/// Отрисовка галереи изображений
/// </summary>
private void DrawGallery(Rect rect)
{
if (portraitTextures.Count == 0)
{
Text.Anchor = TextAnchor.MiddleCenter;
GUI.color = new Color(1f, 1f, 1f, 0.5f);
Widgets.Label(rect, "AIImages.Gallery.Empty".Translate());
GUI.color = Color.white;
Text.Anchor = TextAnchor.UpperLeft;
return;
}
// Параметры сетки
int columns = 3;
int rows = Mathf.CeilToInt((float)portraitTextures.Count / columns);
float cellWidth = (rect.width - 40f) / columns;
float cellHeight = cellWidth + 40f; // Высота ячейки (изображение + кнопки)
float spacing = 10f;
float viewHeight = rows * cellHeight + spacing;
Rect viewRect = new Rect(0f, 0f, rect.width - 20f, viewHeight);
Widgets.BeginScrollView(rect, ref mainScrollPosition, viewRect);
for (int i = 0; i < portraitTextures.Count; i++)
{
int row = i / columns;
int col = i % columns;
float x = col * (cellWidth + spacing);
float y = row * cellHeight;
Rect cellRect = new Rect(x, y, cellWidth, cellHeight);
DrawGalleryItem(cellRect, i);
}
Widgets.EndScrollView();
}
/// <summary>
/// Отрисовка одного элемента галереи
/// </summary>
private void DrawGalleryItem(Rect rect, int index)
{
// Подсветка выбранного элемента
if (index == selectedIndex)
{
Widgets.DrawBoxSolid(rect, new Color(0.3f, 0.5f, 0.8f, 0.2f));
Widgets.DrawBox(rect, 2);
}
else
{
Widgets.DrawBox(rect);
}
// Изображение
Texture2D texture = portraitTextures[index];
if (texture != null)
{
Rect imageRect = new Rect(
rect.x + 5f,
rect.y + 5f,
rect.width - 10f,
rect.width - 10f
);
// Клик по изображению для выбора
if (Widgets.ButtonInvisible(imageRect))
{
selectedIndex = index;
}
GUI.DrawTexture(imageRect, texture);
}
else
{
Rect errorRect = new Rect(rect.x + 5f, rect.y + 5f, rect.width - 10f, rect.width - 10f);
Widgets.DrawBoxSolid(errorRect, new Color(0.5f, 0f, 0f, 0.5f));
Text.Anchor = TextAnchor.MiddleCenter;
GUI.color = Color.white;
Widgets.Label(errorRect, "AIImages.Gallery.LoadError".Translate());
GUI.color = Color.white;
Text.Anchor = TextAnchor.UpperLeft;
}
// Информация под изображением
Text.Font = GameFont.Tiny;
Text.Anchor = TextAnchor.UpperLeft;
float infoY = rect.y + rect.width - 5f;
// Имя файла
string fileName = Path.GetFileName(portraitPaths[index]);
if (fileName.Length > 30)
{
fileName = fileName.Substring(0, 27) + "...";
}
Widgets.Label(
new Rect(rect.x + 5f, infoY, rect.width - 10f, 20f),
fileName
);
// Дата создания
if (File.Exists(portraitPaths[index]))
{
try
{
var fileInfo = new FileInfo(portraitPaths[index]);
string dateStr = fileInfo.CreationTime.ToString("dd.MM.yyyy HH:mm");
Widgets.Label(
new Rect(rect.x + 5f, infoY + 15f, rect.width - 10f, 20f),
dateStr
);
}
catch
{
// Игнорируем ошибки чтения даты
}
}
}
/// <summary>
/// Удаляет выбранное изображение
/// </summary>
private void DeleteSelectedImage()
{
if (selectedIndex < 0 || selectedIndex >= portraitPaths.Count)
return;
string path = portraitPaths[selectedIndex];
// Подтверждение
Find.WindowStack.Add(
Dialog_MessageBox.CreateConfirmation(
"AIImages.Gallery.ConfirmDelete".Translate(),
delegate
{
// Удаляем из файловой системы
if (File.Exists(path))
{
try
{
File.Delete(path);
DebugLogger.Log($"[AI Gallery] Deleted file: {path}");
}
catch (System.Exception ex)
{
Messages.Message(
"AIImages.Gallery.DeleteError".Translate(ex.Message),
MessageTypeDefOf.RejectInput
);
return;
}
}
// Удаляем из компонента пешки
PawnPortraitHelper.RemovePortrait(pawn, path);
// Обновляем галерею
LoadGallery();
// Сбрасываем выбор
if (selectedIndex >= portraitTextures.Count)
{
selectedIndex = portraitTextures.Count - 1;
}
Messages.Message(
"AIImages.Gallery.Deleted".Translate(),
MessageTypeDefOf.PositiveEvent
);
},
destructive: true
)
);
}
/// <summary>
/// Удаляет все изображения
/// </summary>
private void DeleteAllImages()
{
// Подтверждение
Find.WindowStack.Add(
Dialog_MessageBox.CreateConfirmation(
"AIImages.Gallery.ConfirmDeleteAll".Translate(portraitPaths.Count),
delegate
{
// Удаляем все файлы
int deletedCount = 0;
foreach (var path in portraitPaths)
{
if (File.Exists(path))
{
try
{
File.Delete(path);
deletedCount++;
}
catch (System.Exception ex)
{
DebugLogger.Warning(
$"[AI Gallery] Failed to delete file: {path}, Error: {ex.Message}"
);
}
}
}
// Очищаем компонент пешки
PawnPortraitHelper.ClearPortrait(pawn);
// Обновляем галерею
LoadGallery();
selectedIndex = 0;
Messages.Message(
"AIImages.Gallery.AllDeleted".Translate(deletedCount),
MessageTypeDefOf.PositiveEvent
);
},
destructive: true
)
);
}
}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.