Add scene fragments to portrait prompts after clothing.
Thing, room, yard and climate precipitation can contribute a short prompt when they are in frame, with a budget so chairs stay silent. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -454,8 +454,14 @@ public sealed class CatalogLoader
|
||||
ValidateColor(color);
|
||||
}
|
||||
|
||||
foreach (var thing in catalog.Things.Values)
|
||||
{
|
||||
PromptContributionValidator.Validate($"ThingDef '{thing.DefName}'", thing.Prompt);
|
||||
}
|
||||
|
||||
foreach (var room in catalog.Rooms.Values)
|
||||
{
|
||||
PromptContributionValidator.Validate($"RoomDef '{room.DefName}'", room.Prompt);
|
||||
foreach (var slot in room.Slots)
|
||||
{
|
||||
if (!catalog.Things.ContainsKey(slot.Thing))
|
||||
@@ -532,6 +538,7 @@ public sealed class CatalogLoader
|
||||
|
||||
foreach (var territory in catalog.Territories.Values)
|
||||
{
|
||||
PromptContributionValidator.Validate($"TerritoryDef '{territory.DefName}'", territory.Prompt);
|
||||
if (!territory.Abstract && territory.TravelMinutes <= 0)
|
||||
{
|
||||
throw new ContentLoadException($"TerritoryDef '{territory.DefName}' travelMinutes must be positive.");
|
||||
|
||||
@@ -120,6 +120,9 @@ public sealed class ThingDef : Def
|
||||
/// are granted per subject instead). Apparel ignores this and fills layers.
|
||||
/// </summary>
|
||||
public float CarryChance { get; init; }
|
||||
|
||||
/// <summary>Portrait fragment when this thing is in the person's frame. Null or empty — silent.</summary>
|
||||
public PromptContribution? Prompt { get; init; }
|
||||
}
|
||||
|
||||
public sealed class PositionDef : Def;
|
||||
@@ -169,6 +172,9 @@ public sealed class RoomDef : Def
|
||||
/// When set, pupil lockers in this room go to students of that sex (<c>male</c> / <c>female</c>).
|
||||
/// </summary>
|
||||
public string? LockerSex { get; init; }
|
||||
|
||||
/// <summary>Portrait fragment when the person is in a room of this def. Null or empty — silent.</summary>
|
||||
public PromptContribution? Prompt { get; init; }
|
||||
}
|
||||
|
||||
public sealed class BuildingDef : Def;
|
||||
@@ -179,4 +185,7 @@ public sealed class TerritoryDef : Def
|
||||
{
|
||||
/// <summary>Game minutes spent occupying the yard when walking through it.</summary>
|
||||
public float TravelMinutes { get; init; }
|
||||
|
||||
/// <summary>Portrait fragment when the person is in this yard. Null or empty — silent.</summary>
|
||||
public PromptContribution? Prompt { get; init; }
|
||||
}
|
||||
|
||||
@@ -883,6 +883,8 @@ internal static class PeopleDefValidator
|
||||
{
|
||||
throw new ContentLoadException($"ClimatePresetDef '{preset.DefName}' insulationPerC cannot be negative.");
|
||||
}
|
||||
|
||||
PromptContributionValidator.ValidateClimate($"ClimatePresetDef '{preset.DefName}'", preset.Prompt);
|
||||
}
|
||||
|
||||
private static void ValidateNameSet(NameSetDef names, DefCatalog catalog, string countryDefName)
|
||||
|
||||
@@ -801,4 +801,7 @@ public sealed class ClimatePresetDef : Def
|
||||
|
||||
/// <summary>How many °C of protection one insulation point is worth.</summary>
|
||||
public float InsulationPerC { get; init; } = 1f;
|
||||
|
||||
/// <summary>Outdoor portrait fragments for clear / rain / snow. Indoor portraits ignore this.</summary>
|
||||
public ClimatePromptSet? Prompt { get; init; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
namespace HSchool.Content;
|
||||
|
||||
/// <summary>One LoRA or embedding a scene def may add to a portrait. Empty name is ignored.</summary>
|
||||
public sealed class PromptLoraRef
|
||||
{
|
||||
public string Name { get; init; } = "";
|
||||
|
||||
public float Weight { get; init; } = 1f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optional portrait-prompt fragment on a thing, room, yard or precipitation. Missing or empty
|
||||
/// means the def is silent — a chair does not name itself in the prompt.
|
||||
/// </summary>
|
||||
public sealed class PromptContribution
|
||||
{
|
||||
public string Positive { get; init; } = "";
|
||||
|
||||
public string Negative { get; init; } = "";
|
||||
|
||||
public float Weight { get; init; } = 1f;
|
||||
|
||||
public IReadOnlyList<PromptLoraRef> PositiveLoras { get; init; } = [];
|
||||
|
||||
public IReadOnlyList<PromptLoraRef> NegativeLoras { get; init; } = [];
|
||||
|
||||
public IReadOnlyList<PromptLoraRef> PositiveEmbeddings { get; init; } = [];
|
||||
|
||||
public IReadOnlyList<PromptLoraRef> NegativeEmbeddings { get; init; } = [];
|
||||
|
||||
public bool IsSilent =>
|
||||
string.IsNullOrWhiteSpace(Positive)
|
||||
&& string.IsNullOrWhiteSpace(Negative)
|
||||
&& !HasNamed(PositiveLoras)
|
||||
&& !HasNamed(NegativeLoras)
|
||||
&& !HasNamed(PositiveEmbeddings)
|
||||
&& !HasNamed(NegativeEmbeddings);
|
||||
|
||||
private static bool HasNamed(IReadOnlyList<PromptLoraRef> entries)
|
||||
{
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(entry.Name))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Clear / rain / snow fragments on a climate preset. Outdoor portraits pick one.</summary>
|
||||
public sealed class ClimatePromptSet
|
||||
{
|
||||
public PromptContribution? Clear { get; init; }
|
||||
|
||||
public PromptContribution? Rain { get; init; }
|
||||
|
||||
public PromptContribution? Snow { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
namespace HSchool.Content;
|
||||
|
||||
internal static class PromptContributionValidator
|
||||
{
|
||||
public const int MaxFragmentLength = 120;
|
||||
|
||||
public static void Validate(string owner, PromptContribution? contribution)
|
||||
{
|
||||
if (contribution is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ValidateFragment(owner, "positive", contribution.Positive);
|
||||
ValidateFragment(owner, "negative", contribution.Negative);
|
||||
|
||||
if (!float.IsFinite(contribution.Weight) || contribution.Weight < 0f)
|
||||
{
|
||||
throw new ContentLoadException($"{owner} prompt weight must be a finite number ≥ 0.");
|
||||
}
|
||||
|
||||
ValidateLoras(owner, "positive LoRA", contribution.PositiveLoras);
|
||||
ValidateLoras(owner, "negative LoRA", contribution.NegativeLoras);
|
||||
ValidateLoras(owner, "positive embedding", contribution.PositiveEmbeddings);
|
||||
ValidateLoras(owner, "negative embedding", contribution.NegativeEmbeddings);
|
||||
}
|
||||
|
||||
public static void ValidateClimate(string owner, ClimatePromptSet? prompts)
|
||||
{
|
||||
if (prompts is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Validate($"{owner} clear", prompts.Clear);
|
||||
Validate($"{owner} rain", prompts.Rain);
|
||||
Validate($"{owner} snow", prompts.Snow);
|
||||
}
|
||||
|
||||
private static void ValidateFragment(string owner, string side, string value)
|
||||
{
|
||||
if (value.Length > MaxFragmentLength)
|
||||
{
|
||||
throw new ContentLoadException(
|
||||
$"{owner} prompt {side} must be at most {MaxFragmentLength} characters.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateLoras(string owner, string side, IReadOnlyList<PromptLoraRef> entries)
|
||||
{
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(entry.Name))
|
||||
{
|
||||
throw new ContentLoadException($"{owner} prompt {side} is missing a name.");
|
||||
}
|
||||
|
||||
if (!float.IsFinite(entry.Weight))
|
||||
{
|
||||
throw new ContentLoadException($"{owner} prompt {side} '{entry.Name}' weight must be finite.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user