diff --git a/1.6/Assemblies/FactionLoadout.dll b/1.6/Assemblies/FactionLoadout.dll index 4b6fd84..1a633c3 100644 Binary files a/1.6/Assemblies/FactionLoadout.dll and b/1.6/Assemblies/FactionLoadout.dll differ diff --git a/1.6/Source/DefCache.cs b/1.6/Source/DefCache.cs index 6e30a6d..296728b 100644 --- a/1.6/Source/DefCache.cs +++ b/1.6/Source/DefCache.cs @@ -221,8 +221,76 @@ private static void PopulateVFEAncientsObjects() public static Dictionary> ApparelBlacklistCache = new(); public static Dictionary> WeaponBlacklistCache = new(); + + /// + /// Pre-cached material rules built at apply time: cloned PawnKindDef → (stuff set, isBlacklist). + /// Whitelist (isBlacklist false) allows only the set; blacklist bans the set. Specific edit's list + /// (and its mode) wins; otherwise the faction's global edit's. Absent (or empty) key = no restriction. + /// + public static Dictionary defs, bool blacklist)> ApparelMaterialCache = new(); + + public static Dictionary defs, bool blacklist)> WeaponMaterialCache = new(); public static RulePackDef FakeRulePack = new() { defName = "NONE" }; + /// True if the kind's apparel material rule permits (no rule / null stuff = allowed). + public static bool ApparelMaterialAllows(PawnKindDef kind, ThingDef stuff) => MaterialAllows(ApparelMaterialCache, kind, stuff); + + /// True if the kind's weapon material rule permits (no rule / null stuff = allowed). + public static bool WeaponMaterialAllows(PawnKindDef kind, ThingDef stuff) => MaterialAllows(WeaponMaterialCache, kind, stuff); + + public static bool MaterialAllows(Dictionary defs, bool blacklist)> cache, PawnKindDef kind, ThingDef stuff) + { + if (stuff == null || !cache.TryGetValue(kind, out (HashSet defs, bool blacklist) rule)) + return true; + bool listed = rule.defs.Contains(stuff); + return rule.blacklist ? !listed : listed; + } + + /// + /// Builds a per-stuff-category count of the materials a list+mode actually permits + /// (e.g. "Leathery: 13 Metallic: 4"), for the editor summary. Returns null if nothing is permitted. + /// + public static string MaterialCategorySummary(List> materials, bool isBlacklist) + { + IEnumerable allowed; + if (isBlacklist) + { + // Blacklist: allowed = every stuff except the banned ones, so we need the set + full scan. + HashSet banned = new(); + if (materials != null) + { + foreach (DefRef r in materials) + { + if (r.HasValue) + banned.Add(r.Def); + } + } + allowed = GenStuff.StuffDefs.Where(s => !banned.Contains(s)); + } + else + { + // Whitelist: allowed = exactly the selected materials — no set or full scan needed. + allowed = (materials ?? Enumerable.Empty>()).Where(r => r.HasValue).Select(r => r.Def); + } + + Dictionary counts = new(); + foreach (ThingDef s in allowed) + { + if (s?.stuffProps?.categories == null) + continue; + foreach (StuffCategoryDef cat in s.stuffProps.categories) + { + counts.TryGetValue(cat, out int c); + counts[cat] = c + 1; + } + } + + if (counts.Count == 0) + return null; + + return string.Join(" ", counts.OrderByDescending(kv => kv.Value).Select(kv => $"{kv.Key.LabelCap}: {kv.Value}")); + } + public static void BuildBlacklistCaches(PawnKindEdit edit, PawnKindDef def, PawnKindEdit global) { HashSet apparelBl = (global?.ApparelBlacklist ?? Enumerable.Empty>()) @@ -254,5 +322,32 @@ public static void BuildBlacklistCaches(PawnKindEdit edit, PawnKindDef def, Pawn { DefCache.WeaponBlacklistCache.Remove(def); } + + // Material rules: specific edit's list (and its mode) wins, else the faction's global edit's. + // Resolve to currently-present stuff defs; missing-mod entries stay in the saved DefRef list + // but simply don't constrain generation until their mod returns. + List> apparelMatList = edit.ApparelMaterials ?? global?.ApparelMaterials; + bool apparelMatBlacklist = edit.ApparelMaterials != null ? edit.ApparelMaterialsBlacklist : global?.ApparelMaterialsBlacklist ?? false; + HashSet apparelMat = apparelMatList?.Where(r => r.HasValue).Select(r => r.Def).ToHashSet(); + if (apparelMat is { Count: > 0 }) + { + DefCache.ApparelMaterialCache[def] = (apparelMat, apparelMatBlacklist); + } + else + { + DefCache.ApparelMaterialCache.Remove(def); + } + + List> weaponMatList = edit.WeaponMaterials ?? global?.WeaponMaterials; + bool weaponMatBlacklist = edit.WeaponMaterials != null ? edit.WeaponMaterialsBlacklist : global?.WeaponMaterialsBlacklist ?? false; + HashSet weaponMat = weaponMatList?.Where(r => r.HasValue).Select(r => r.Def).ToHashSet(); + if (weaponMat is { Count: > 0 }) + { + DefCache.WeaponMaterialCache[def] = (weaponMat, weaponMatBlacklist); + } + else + { + DefCache.WeaponMaterialCache.Remove(def); + } } } diff --git a/1.6/Source/MySettings.cs b/1.6/Source/MySettings.cs index ec54054..c0f5f22 100644 --- a/1.6/Source/MySettings.cs +++ b/1.6/Source/MySettings.cs @@ -8,6 +8,7 @@ public class MySettings : ModSettings public static bool VanillaRestrictions = true; public static bool VerboseLogging = false; public static bool PatchKindInRequests = false; + public static bool IgnorePriceLimits = false; public override void ExposeData() { @@ -17,5 +18,6 @@ public override void ExposeData() Scribe_Values.Look(ref VanillaRestrictions, "vanillaRestrictions", true); Scribe_Values.Look(ref VerboseLogging, "verboseLogging", false); Scribe_Values.Look(ref PatchKindInRequests, "patchKindInRequests", false); + Scribe_Values.Look(ref IgnorePriceLimits, "ignorePriceLimits", false); } } diff --git a/1.6/Source/Patches/ApparelGenPatch.cs b/1.6/Source/Patches/ApparelGenPatch.cs index bf3b9ae..720515f 100644 --- a/1.6/Source/Patches/ApparelGenPatch.cs +++ b/1.6/Source/Patches/ApparelGenPatch.cs @@ -60,6 +60,10 @@ private static void Postfix(Pawn pawn) if (edits.editCount > 0 && pawn.RaceProps.ToolUser) ForceGiveClothes(pawn, edits); + // For TC-managed kinds, surface (and optionally fix) a bare torso left by a too-low budget. + if (edits.editCount > 0 && !edits.anyForceNaked) + HandleApparelPriceLimit(pawn); + HairDef hair = GetForcedHair(edits); BeardDef beard = GetForcedBeard(edits); Color? color = GetForcedHairColor(edits); @@ -252,6 +256,98 @@ private static BeardDef GetForcedBeard(AccumulatedApparelEdits edits) c.a = 1f; return c; } + + // ==================== Price-limited apparel fallback ==================== + + /// + /// When a TC-managed pawn ends up with no torso apparel, determines whether price was the + /// limiting factor (an allowed torso item exists but costs more than the budget) and, if so, + /// logs it (verbose) and optionally wears the cheapest matching item (). + /// + private static void HandleApparelPriceLimit(Pawn pawn) + { + // Nothing to log or fix when both toggles are off — skip the allApparelPairs scan entirely. + if (!MySettings.VerboseLogging && !MySettings.IgnorePriceLimits) + return; + + if (pawn.apparel == null || !pawn.RaceProps.ToolUser || !pawn.RaceProps.IsFlesh) + return; + if (CoversTorso(pawn)) + return; + + ThingStuffPair? cheapest = CheapestEligibleTorsoApparel(pawn); + if (cheapest == null) + return; // No allowed torso apparel exists → the cause is tags/filters, not price. + + ThingStuffPair pair = cheapest.Value; + + // Only act when price was genuinely the limiter: the cheapest allowed torso item must exceed the + // budget. If it was affordable, the bare torso came from something else and isn't ours to "fix". + if (pair.Price <= pawn.kindDef.apparelMoney.max) + return; + + if (MySettings.VerboseLogging) + { + string mat = pair.stuff != null ? $" ({pair.stuff.LabelCap})" : ""; + ModCore.Warn( + $"Apparel slot left empty by price for '{pawn.kindDef.LabelCap}': no torso apparel within apparelMoney {pawn.kindDef.apparelMoney}. " + + $"Cheapest matching option is {pair.thing.LabelCap}{mat} at ${pair.Price:F0} — raise apparelMoney or relax the apparel/material filters." + ); + } + + if (MySettings.IgnorePriceLimits) + WearFallbackApparel(pawn, pair); + } + + private static bool CoversTorso(Pawn pawn) + { + List worn = pawn.apparel.WornApparel; + for (int i = 0; i < worn.Count; i++) + { + List groups = worn[i].def.apparel?.bodyPartGroups; + if (groups != null && groups.Contains(BodyPartGroupDefOf.Torso)) + return true; + } + + return false; + } + + private static ThingStuffPair? CheapestEligibleTorsoApparel(Pawn pawn) + { + ThingStuffPair? best = null; + List pairs = PawnApparelGenerator.allApparelPairs; + for (int i = 0; i < pairs.Count; i++) + { + ThingStuffPair p = pairs[i]; + List groups = p.thing.apparel?.bodyPartGroups; + if (groups == null || !groups.Contains(BodyPartGroupDefOf.Torso)) + continue; + // CanUsePair (with unlimited money) applies all of vanilla's filters plus our own + // blacklist/material postfix, so we reuse it rather than duplicating that logic. + if (!PawnApparelGenerator.CanUsePair(p, pawn, float.MaxValue, true, pawn.thingIDNumber)) + continue; + if (best == null || p.Price < best.Value.Price) + best = p; + } + + return best; + } + + private static void WearFallbackApparel(Pawn pawn, ThingStuffPair pair) + { + try + { + if (ThingMaker.MakeThing(pair.thing, pair.stuff) is not Apparel apparel) + return; + PawnApparelGenerator.PostProcessApparel(apparel, pawn); + pawn.apparel.Wear(apparel, false); + ModCore.Debug($"Ignore-price fallback dressed '{pawn.kindDef.LabelCap}' in {apparel.LabelCap}."); + } + catch (Exception e) + { + ModCore.Error($"Ignore-price apparel fallback failed for '{pawn.kindDef.LabelCap}'", e); + } + } } /// @@ -268,6 +364,28 @@ static void Postfix(ThingStuffPair pair, Pawn pawn, ref bool __result) return; if (DefCache.ApparelBlacklistCache.TryGetValue(pawn.kindDef, out HashSet bl) && bl.Contains(pair.thing)) + { + __result = false; + return; + } + + // Material rule (whitelist or blacklist) — keeps disallowed materials out of the candidate pool. + if (!DefCache.ApparelMaterialAllows(pawn.kindDef, pair.stuff)) + __result = false; + } +} + +/// +/// Applies the per-kind apparel material rule to vanilla's stuff gate. REQUIRED apparel +/// (PawnKindDef.apparelRequired) picks its stuff via CanUseStuff and never touches CanUsePair, +/// so without this a required item could spawn in a disallowed material. +/// +[HarmonyPatch(typeof(PawnApparelGenerator), "CanUseStuff")] +public static class CanUseStuffMaterialPatch +{ + static void Postfix(Pawn pawn, ThingStuffPair pair, ref bool __result) + { + if (__result && !DefCache.ApparelMaterialAllows(pawn.kindDef, pair.stuff)) __result = false; } } diff --git a/1.6/Source/Patches/WeaponGenPatch.cs b/1.6/Source/Patches/WeaponGenPatch.cs index a6dfbc7..782dc59 100644 --- a/1.6/Source/Patches/WeaponGenPatch.cs +++ b/1.6/Source/Patches/WeaponGenPatch.cs @@ -41,6 +41,10 @@ static void Postfix(Pawn pawn) if (edits.editCount > 0 && pawn.RaceProps.ToolUser) ForceGiveWeapons(pawn, edits); + + // For TC-managed kinds, surface (and optionally fix) weapon slots left empty by a too-low budget. + if (edits.editCount > 0) + HandleWeaponPriceLimit(pawn); } static void ForceGiveWeapons(Pawn pawn, AccumulatedWeaponEdits edits) @@ -178,6 +182,114 @@ static ThingWithComps GenerateNewWeapon(Pawn pawn, SpecRequirementEdit spec) return thing; } + + // ==================== Price-limited weapon fallback ==================== + + /// + /// When a TC-managed pawn ends up with no primary weapon, determines whether price was the + /// limiting factor (matching weapons exist but none were affordable) and, if so, logs it + /// (verbose) and optionally equips the cheapest matching weapon (). + /// + static void HandleWeaponPriceLimit(Pawn pawn) + { + // Nothing to log or fix when both toggles are off — skip the allWeaponPairs scan entirely. + if (!MySettings.VerboseLogging && !MySettings.IgnorePriceLimits) + return; + + if (pawn.equipment == null || pawn.equipment.Primary != null) + return; + + PawnKindDef kind = pawn.kindDef; + if (kind.weaponTags == null || kind.weaponTags.Count == 0) + return; + // Mirror Postfix's guards exactly so the fallback enforces the same restrictions: the tool-user + // and violent checks only apply when vanilla restrictions are enabled. + if (MySettings.VanillaRestrictions && !pawn.RaceProps.ToolUser) + return; + if (!pawn.health.capacities.CapableOf(PawnCapacityDefOf.Manipulation)) + return; + if (MySettings.VanillaRestrictions && pawn.WorkTagIsDisabled(WorkTags.Violent)) + return; + + ThingStuffPair? cheapest = null; + List pairs = PawnWeaponGenerator.allWeaponPairs; + for (int i = 0; i < pairs.Count; i++) + { + ThingStuffPair w = pairs[i]; + if (!WeaponMatchesKind(w, pawn, kind)) + continue; + // GetCommonality already includes our blacklist/material zeroing, so a positive value means TC allows it. + if (PawnWeaponGenerator.GetCommonality(pawn, w) <= 0f) + continue; + if (cheapest == null || w.Price < cheapest.Value.Price) + cheapest = w; + } + + // No matching weapon at all → the cause is tags/filters, not price. Stay quiet. + if (cheapest == null) + return; + + ThingStuffPair pair = cheapest.Value; + + // Only act when price was genuinely the limiter: the cheapest matching weapon must exceed the + // budget. Otherwise the pawn went unarmed for another reason (RNG, another mod) and attributing + // it to budget — or "fixing" it — would be wrong. + if (pair.Price <= kind.weaponMoney.max) + return; + + if (MySettings.VerboseLogging) + { + string mat = pair.stuff != null ? $" ({pair.stuff.LabelCap})" : ""; + ModCore.Warn( + $"Weapon slot left empty by price for '{pawn.kindDef.LabelCap}': nothing affordable within weaponMoney {kind.weaponMoney}. " + + $"Cheapest matching option is {pair.thing.LabelCap}{mat} at ${pair.Price:F0} — raise weaponMoney or relax the weapon/material filters." + ); + } + + if (MySettings.IgnorePriceLimits) + EquipFallbackWeapon(pawn, pair); + } + + static bool WeaponMatchesKind(ThingStuffPair w, Pawn pawn, PawnKindDef kind) + { + bool tagMatch = false; + for (int i = 0; i < kind.weaponTags.Count; i++) + { + if (w.thing.weaponTags != null && w.thing.weaponTags.Contains(kind.weaponTags[i])) + { + tagMatch = true; + break; + } + } + + if (!tagMatch) + return false; + if (kind.weaponStuffOverride != null && w.stuff != kind.weaponStuffOverride) + return false; + if (w.thing.IsRangedWeapon && pawn.WorkTagIsDisabled(WorkTags.Shooting)) + return false; + if (w.stuff != null && !w.stuff.stuffProps.allowedInStuffGeneration) + return false; + return true; + } + + static void EquipFallbackWeapon(Pawn pawn, ThingStuffPair pair) + { + try + { + if (ThingMaker.MakeThing(pair.thing, pair.stuff) is not ThingWithComps weapon) + return; + PawnGenerator.PostProcessGeneratedGear(weapon, pawn); + if (pawn.equipment.Primary != null) + pawn.equipment.Remove(pawn.equipment.Primary); + pawn.equipment.AddEquipment(weapon); + ModCore.Debug($"Ignore-price fallback armed '{pawn.kindDef.LabelCap}' with {weapon.LabelCap}."); + } + catch (Exception e) + { + ModCore.Error($"Ignore-price weapon fallback failed for '{pawn.kindDef.LabelCap}'", e); + } + } } /// @@ -195,6 +307,13 @@ static void Postfix(Pawn pawn, ThingStuffPair pair, ref float __result) return; if (DefCache.WeaponBlacklistCache.TryGetValue(pawn.kindDef, out HashSet bl) && bl.Contains(pair.thing)) + { + __result = 0f; + return; + } + + // Material rule (whitelist or blacklist): zero out stuff-based weapons the kind's rule disallows. + if (!DefCache.WeaponMaterialAllows(pawn.kindDef, pair.stuff)) __result = 0f; } } diff --git a/1.6/Source/PawnKindEdit.cs b/1.6/Source/PawnKindEdit.cs index 451a222..af26758 100644 --- a/1.6/Source/PawnKindEdit.cs +++ b/1.6/Source/PawnKindEdit.cs @@ -126,6 +126,10 @@ public static void AddActiveEdit(PawnKindDef def, PawnKindEdit edit) public List ApparelDisallowedTags = null; public List> ApparelBlacklist = null; public List> WeaponBlacklist = null; + public List> ApparelMaterials = null; + public bool ApparelMaterialsBlacklist = false; + public List> WeaponMaterials = null; + public bool WeaponMaterialsBlacklist = false; public List> ApparelRequired = null; public List> TechRequired = null; public List SpecificApparel = null; @@ -233,6 +237,10 @@ public void ExposeData() Scribe_Collections.Look(ref ApparelDisallowedTags, "apparelDisallowedTags"); Scribe_Collections.Look(ref ApparelBlacklist, "apparelBlacklist", LookMode.Deep); Scribe_Collections.Look(ref WeaponBlacklist, "weaponBlacklist", LookMode.Deep); + Scribe_Collections.Look(ref ApparelMaterials, "apparelMaterials", LookMode.Deep); + Scribe_Collections.Look(ref WeaponMaterials, "weaponMaterials", LookMode.Deep); + Scribe_Values.Look(ref ApparelMaterialsBlacklist, "apparelMaterialsBlacklist", false); + Scribe_Values.Look(ref WeaponMaterialsBlacklist, "weaponMaterialsBlacklist", false); ScribeMigrateDefRefList(ref ApparelRequired, "apparelRequired"); ScribeMigrateDefRefList(ref TechRequired, "techRequired"); Scribe_Collections.Look(ref SpecificApparel, "specificApparel", LookMode.Deep); diff --git a/1.6/Source/Tabs/ApparelTab.cs b/1.6/Source/Tabs/ApparelTab.cs index d166fdf..aaf321d 100644 --- a/1.6/Source/Tabs/ApparelTab.cs +++ b/1.6/Source/Tabs/ApparelTab.cs @@ -77,6 +77,21 @@ protected override void DrawContents(Listing_Standard ui) false, pasteGet: e => e.ApparelBlacklist ); + DrawOverride( + ui, + null, + ref Current.ApparelMaterials, + "FactionLoadout_ApparelMaterials".Translate(), + DrawApparelMaterials, + GetHeightFor(Current.ApparelMaterials) + 26f, + false, + pasteGet: e => + { + Current.ApparelMaterialsBlacklist = e.ApparelMaterialsBlacklist; + return e.ApparelMaterials; + } + ); + DrawMaterialSummary(ui, Current.ApparelMaterials, Current.ApparelMaterialsBlacklist); } private void DrawForceOnlySelected(Listing_Standard ui) @@ -170,8 +185,66 @@ private void DrawApparelBlacklist(Rect rect, bool active, System.Collections.Gen DrawDefRefList(rect, active, ref scrolls[scrollIndex++], Current.ApparelBlacklist, null, DefCache.AllApparel); } + private void DrawApparelMaterials(Rect rect, bool active, System.Collections.Generic.List> defaultList) + { + Rect listRect = DrawMaterialModeToggle(rect, ref Current.ApparelMaterialsBlacklist); + DrawDefRefList(listRect, active, ref scrolls[scrollIndex++], Current.ApparelMaterials, null, GenStuff.StuffDefs); + } + private void DrawRequiredApparel(Rect rect, bool active, System.Collections.Generic.List> _) { - DrawDefRefList(rect, active, ref scrolls[scrollIndex++], Current.ApparelRequired, DefaultKind.apparelRequired, DefCache.AllApparel); + DrawDefRefList( + rect, + active, + ref scrolls[scrollIndex++], + Current.ApparelRequired, + DefaultKind.apparelRequired, + DefCache.AllApparel, + warningFunc: RequiredApparelMaterialWarning + ); + } + + private string RequiredApparelMaterialWarning(ThingDef item) + { + if (item == null || !item.MadeFromStuff) + return null; + + // Mirror generation-time resolution (DefCache.BuildBlacklistCaches): the per-kind list wins, + // otherwise the faction's global edit's list applies — so the warning must honour the global + // rule too, or it would miss required items the global material rule will actually skip. + List> rule = Current.ApparelMaterials; + bool blacklist = Current.ApparelMaterialsBlacklist; + if ((rule == null || rule.Count == 0) && !Current.IsGlobal) + { + PawnKindEdit global = Find.WindowStack.WindowOfType()?.Current?.GetGlobalEditor(); + if (global != null) + { + rule = global.ApparelMaterials; + blacklist = global.ApparelMaterialsBlacklist; + } + } + + if (rule == null || rule.Count == 0) + return null; + + foreach (ThingDef stuff in GenStuff.AllowedStuffsFor(item)) + { + bool listed = RuleContains(rule, stuff); + if (blacklist ? !listed : listed) + return null; // at least one allowed material can make this item + } + + return "FactionLoadout_Materials_NoValidStuff".Translate(); + } + + private static bool RuleContains(List> rule, ThingDef stuff) + { + for (int i = 0; i < rule.Count; i++) + { + if (rule[i].HasValue && rule[i].Def == stuff) + return true; + } + + return false; } } diff --git a/1.6/Source/Tabs/WeaponTab.cs b/1.6/Source/Tabs/WeaponTab.cs index 3b17b74..f25d7e2 100644 --- a/1.6/Source/Tabs/WeaponTab.cs +++ b/1.6/Source/Tabs/WeaponTab.cs @@ -59,6 +59,21 @@ protected override void DrawContents(Listing_Standard ui) false, pasteGet: e => e.WeaponBlacklist ); + DrawOverride( + ui, + null, + ref Current.WeaponMaterials, + "FactionLoadout_WeaponMaterials".Translate(), + DrawWeaponMaterials, + GetHeightFor(Current.WeaponMaterials) + 26f, + false, + pasteGet: e => + { + Current.WeaponMaterialsBlacklist = e.WeaponMaterialsBlacklist; + return e.WeaponMaterials; + } + ); + DrawMaterialSummary(ui, Current.WeaponMaterials, Current.WeaponMaterialsBlacklist); } private void DrawWeaponQuality(Rect rect, bool active, QualityCategory _) @@ -85,4 +100,10 @@ private void DrawWeaponBlacklist(Rect rect, bool active, System.Collections.Gene { DrawDefRefList(rect, active, ref scrolls[scrollIndex++], Current.WeaponBlacklist, null, DefCache.AllWeapons); } + + private void DrawWeaponMaterials(Rect rect, bool active, System.Collections.Generic.List> defaultList) + { + Rect listRect = DrawMaterialModeToggle(rect, ref Current.WeaponMaterialsBlacklist); + DrawDefRefList(listRect, active, ref scrolls[scrollIndex++], Current.WeaponMaterials, null, GenStuff.StuffDefs); + } } diff --git a/1.6/Source/UISupport/Dialog_FactionLoadout.cs b/1.6/Source/UISupport/Dialog_FactionLoadout.cs index 969b1b3..164bf84 100644 --- a/1.6/Source/UISupport/Dialog_FactionLoadout.cs +++ b/1.6/Source/UISupport/Dialog_FactionLoadout.cs @@ -52,6 +52,7 @@ public override void DoWindowContents(Rect inRect) ref MySettings.PatchKindInRequests, "FactionLoadout_Settings_PatchKindInRequestsDesc".Translate() ); + ui.CheckboxLabeled("FactionLoadout_Settings_IgnorePrice".Translate(), ref MySettings.IgnorePriceLimits, "FactionLoadout_Settings_IgnorePriceDesc".Translate()); ui.GapLine(); ui.Label("FactionLoadout_Settings_FactionPresetDesc".Translate()); ui.GapLine(); diff --git a/1.6/Source/UISupport/DrawSupport/ListDrawSupport.cs b/1.6/Source/UISupport/DrawSupport/ListDrawSupport.cs index 529893b..8aca127 100644 --- a/1.6/Source/UISupport/DrawSupport/ListDrawSupport.cs +++ b/1.6/Source/UISupport/DrawSupport/ListDrawSupport.cs @@ -24,7 +24,8 @@ public static CustomFloatMenu DrawDefRefList( IEnumerable allDefs, bool isGlobal, Func makeItem = null, - Func labelFunc = null + Func labelFunc = null, + Func warningFunc = null ) where T : Def, new() { @@ -74,6 +75,10 @@ string MakeDefaultString(IList list) DefRef toRemove = null; foreach (DefRef defRef in current) { + string warning = defRef.HasValue ? warningFunc?.Invoke(defRef.Def) : null; + if (!string.IsNullOrEmpty(warning)) + Widgets.DrawBoxSolid(new Rect(1, curr.y - 1, rect.width - 6, 24), new Color(0.7f, 0.2f, 0.2f, 0.28f)); + GUI.color = Color.red; if (Widgets.ButtonText(currButton, " X")) toRemove = defRef; @@ -107,6 +112,9 @@ string MakeDefaultString(IList list) } } + if (!string.IsNullOrEmpty(warning)) + TooltipHandler.TipRegion(new Rect(1, curr.y - 1, rect.width - 6, 24), warning); + curr.y += 26; currButton.y += 26; } diff --git a/1.6/Source/UISupport/EditTab.cs b/1.6/Source/UISupport/EditTab.cs index e9dabdc..64453ed 100644 --- a/1.6/Source/UISupport/EditTab.cs +++ b/1.6/Source/UISupport/EditTab.cs @@ -99,9 +99,10 @@ protected CustomFloatMenu DrawDefRefList( IList defaults, IEnumerable allDefs, Func makeItem = null, - Func labelFunc = null + Func labelFunc = null, + Func warningFunc = null ) - where T : Def, new() => ListDrawSupport.DrawDefRefList(rect, active, ref scroll, current, defaults, allDefs, Current.IsGlobal, makeItem, labelFunc); + where T : Def, new() => ListDrawSupport.DrawDefRefList(rect, active, ref scroll, current, defaults, allDefs, Current.IsGlobal, makeItem, labelFunc, warningFunc); protected CustomFloatMenu DrawDefList( Rect rect, @@ -144,6 +145,40 @@ protected void DrawFloatRange(Rect rect, bool active, ref FloatRange? current, F protected void DrawSpecificGear(Listing_Standard ui, ref List edits, string label, Func thingFilter, ThingDef defaultThing) => SpecificGearDrawer.Draw(ui, ref edits, label, thingFilter, defaultThing, ref scrolls[scrollIndex++]); + /// + /// Draws the whitelist/blacklist mode toggle for a material list at the top of + /// and returns the remaining rect below it for the def-list picker. + /// + protected Rect DrawMaterialModeToggle(Rect rect, ref bool isBlacklist) + { + string label = isBlacklist ? "FactionLoadout_Materials_ModeBlacklist".Translate() : "FactionLoadout_Materials_ModeWhitelist".Translate(); + Rect modeRow = rect; + modeRow.height = 24f; + modeRow.width = Mathf.Min(rect.width, Mathf.Max(180f, Text.CalcSize(label).x + 24f)); + if (Widgets.ButtonText(modeRow, label)) + isBlacklist = !isBlacklist; + + Rect listRect = rect; + listRect.yMin += 26f; + return listRect; + } + + /// + /// Draws a one-line "Allowed: Leathery 13, Metallic 4, …" summary of what a material list+mode permits. + /// No-op when the field is inactive (null list) or nothing is permitted. + /// + protected void DrawMaterialSummary(Listing_Standard ui, List> materials, bool isBlacklist) + { + if (materials == null) + return; + string summary = DefCache.MaterialCategorySummary(materials, isBlacklist); + if (string.IsNullOrEmpty(summary)) + return; + GUI.color = new Color(0.62f, 0.78f, 1f); + ui.Label("FactionLoadout_Materials_AllowedSummary".Translate(summary)); + GUI.color = Color.white; + } + public void DrawCurve(Listing_Standard listing, ref SimpleCurve curve, ref List<(string x, string y)> curvePointBuffer) => CurveDrawer.DrawCurve(listing, ref curve, ref curvePointBuffer); } diff --git a/Common/Languages/English/Keyed/FactionLoadout_Keys.xml b/Common/Languages/English/Keyed/FactionLoadout_Keys.xml index a166017..a45988a 100644 --- a/Common/Languages/English/Keyed/FactionLoadout_Keys.xml +++ b/Common/Languages/English/Keyed/FactionLoadout_Keys.xml @@ -44,6 +44,8 @@ Here you can manage faction edit presets.\nEach preset contains a collection of faction edits. Only one preset can be active at a time.\nHold the SHIFT key to delete presets.\nRemember all changes require a game restart to take effect. Patch forced pawn kinds in PawnGen (Restart Required): Patch the pawn kind set in PawnGenerationRequests. Should catch cases where the game generates a pawn without taking the patched version from the faction when generating pawns like for royal permits. Currently experimental. + Ignore price limits when necessary + When a Total Control-managed pawn can't afford any allowed weapon or torso apparel within its budget, equip the cheapest matching option anyway instead of leaving the slot empty. With verbose logging on, such cases are logged even when this is off, so you know to raise the budget. Tech Level Not Overriden ({0}) @@ -278,6 +280,12 @@ Coverage: {0} Apparel Blacklist Weapon Blacklist + Apparel Materials + Weapon Materials + Mode: Whitelist (only these) + Mode: Blacklist (all except these) + Allowed: {0} + No material allowed by this kind's material rule can make this item — it will be skipped when the pawn is generated. Override: <color={0}>{1}</color> diff --git a/Compatibility/CombatExtended/1.6/Assemblies/TotalControlCECompat.dll b/Compatibility/CombatExtended/1.6/Assemblies/TotalControlCECompat.dll index d8009ad..13875e5 100644 Binary files a/Compatibility/CombatExtended/1.6/Assemblies/TotalControlCECompat.dll and b/Compatibility/CombatExtended/1.6/Assemblies/TotalControlCECompat.dll differ diff --git a/Compatibility/GiddyUp/1.6/Assemblies/TotalControlGiddyUpCompat.dll b/Compatibility/GiddyUp/1.6/Assemblies/TotalControlGiddyUpCompat.dll index f3a05fe..43d0bf7 100644 Binary files a/Compatibility/GiddyUp/1.6/Assemblies/TotalControlGiddyUpCompat.dll and b/Compatibility/GiddyUp/1.6/Assemblies/TotalControlGiddyUpCompat.dll differ diff --git a/Compatibility/VEPsycasts/1.6/Assemblies/TotalControlVEPsycastsCompat.dll b/Compatibility/VEPsycasts/1.6/Assemblies/TotalControlVEPsycastsCompat.dll index 4cbc71c..594547a 100644 Binary files a/Compatibility/VEPsycasts/1.6/Assemblies/TotalControlVEPsycastsCompat.dll and b/Compatibility/VEPsycasts/1.6/Assemblies/TotalControlVEPsycastsCompat.dll differ