diff --git a/1.6/Assemblies/0Harmony.dll b/1.6/Assemblies/0Harmony.dll
new file mode 100644
index 0000000..f501506
Binary files /dev/null and b/1.6/Assemblies/0Harmony.dll differ
diff --git a/1.6/Assemblies/0Harmony.pdb b/1.6/Assemblies/0Harmony.pdb
new file mode 100644
index 0000000..fa33809
Binary files /dev/null and b/1.6/Assemblies/0Harmony.pdb differ
diff --git a/1.6/Assemblies/Better Work Tab.dll b/1.6/Assemblies/Better Work Tab.dll
index 31d436b..bdec6a0 100644
Binary files a/1.6/Assemblies/Better Work Tab.dll and b/1.6/Assemblies/Better Work Tab.dll differ
diff --git a/1.6/Assemblies/Better Work Tab.pdb b/1.6/Assemblies/Better Work Tab.pdb
new file mode 100644
index 0000000..bbf6bf7
Binary files /dev/null and b/1.6/Assemblies/Better Work Tab.pdb differ
diff --git a/Languages/English/Keyed/RuleBuilder.xml b/Languages/English/Keyed/RuleBuilder.xml
index b4203cd..dd4bdb2 100644
--- a/Languages/English/Keyed/RuleBuilder.xml
+++ b/Languages/English/Keyed/RuleBuilder.xml
@@ -33,6 +33,8 @@
High priority. This pawn will be assigned before normal priority pawns.
Normal priority. Standard assignment.
Low priority. This pawn will be assigned last.
+ Priority ({0})
+ Extended priority {0}. Higher numbers are lower priority and run after smaller non-zero values.
Skill-Based
diff --git a/Source/API/PriorityApi.cs b/Source/API/PriorityApi.cs
new file mode 100644
index 0000000..2fad8c9
--- /dev/null
+++ b/Source/API/PriorityApi.cs
@@ -0,0 +1,161 @@
+using System;
+using Better_Work_Tab.Features.RaisedPriorityMaximum;
+
+namespace Better_Work_Tab.API
+{
+ ///
+ /// Immutable snapshot of Better Work Tab's current priority configuration.
+ /// Designed to be easy to consume over reflection so other mods do not need
+ /// a hard assembly reference just to read max-priority settings.
+ ///
+ public readonly struct PriorityApiSnapshot
+ {
+ public readonly int ApiVersion;
+ public readonly int MaxPriority;
+ public readonly int DefaultEnabledPriority;
+
+ public PriorityApiSnapshot(int apiVersion, int maxPriority, int defaultEnabledPriority)
+ {
+ ApiVersion = apiVersion;
+ MaxPriority = maxPriority;
+ DefaultEnabledPriority = defaultEnabledPriority;
+ }
+ }
+
+ ///
+ /// Public compatibility surface for Better Work Tab's extended manual priorities.
+ /// Other mods should prefer this API over hardcoding the vanilla max priority of 4.
+ ///
+ /// Reflection integration:
+ /// - Assembly: "Better Work Tab"
+ /// - Type: "Better_Work_Tab.API.PriorityApi"
+ /// - Read methods such as or
+ ///
+ /// Example reflection flow for mods that do not want a hard dependency:
+ /// 1. Find the loaded assembly named "Better Work Tab".
+ /// 2. Resolve type "Better_Work_Tab.API.PriorityApi".
+ /// 3. Invoke static method "GetMaxPriority" or "GetSnapshot".
+ ///
+ public static class PriorityApi
+ {
+ ///
+ /// Stable API version for reflection-based callers.
+ /// Increment only when the public contract changes incompatibly.
+ ///
+ public const int ApiVersion = 1;
+
+ ///
+ /// Stable assembly name for reflection-based lookups.
+ ///
+ public const string AssemblyName = "Better Work Tab";
+
+ ///
+ /// Stable fully-qualified type name for reflection-based lookups.
+ ///
+ public const string TypeName = "Better_Work_Tab.API.PriorityApi";
+
+ ///
+ /// Returns the configured upper bound for manual priorities.
+ /// Guaranteed to return at least 1.
+ ///
+ public static int GetMaxPriority()
+ {
+ try
+ {
+ return MaxPriorityLogic.GetMaxPriority();
+ }
+ catch
+ {
+ return Math.Max(1, DefaultSettings.maxPriority);
+ }
+ }
+
+ ///
+ /// Returns the default priority used when enabling a work type outside manual-priority mode.
+ ///
+ public static int GetDefaultEnabledPriority()
+ {
+ try
+ {
+ return MaxPriorityLogic.GetDefaultEnabledPriority();
+ }
+ catch
+ {
+ return Clamp(3, 1, GetMaxPriority());
+ }
+ }
+
+ ///
+ /// Maps an extended stored priority back into RimWorld's vanilla-style 1..4 display buckets.
+ /// Returns 0 for disabled priorities.
+ ///
+ public static int MapPriorityToVanillaDisplay(int priority)
+ {
+ try
+ {
+ return MaxPriorityLogic.MapPriorityToVanillaDisplay(priority);
+ }
+ catch
+ {
+ if (priority <= 0)
+ {
+ return 0;
+ }
+
+ int maxPriority = GetMaxPriority();
+ if (maxPriority <= 1)
+ {
+ return 1;
+ }
+
+ float remapped = Spine.Utils.SpineUtils.Remap(priority, 1, maxPriority, 1, 4);
+ return Clamp((int)Math.Round(remapped), 1, 4);
+ }
+ }
+
+ ///
+ /// True when the priority value represents "disabled" for a work type.
+ ///
+ public static bool IsDisabledPriority(int priority)
+ {
+ return priority <= 0;
+ }
+
+ ///
+ /// Returns a small versioned snapshot for reflection-friendly integration.
+ /// This is the preferred call for mods that want one round-trip instead of multiple method calls.
+ ///
+ public static PriorityApiSnapshot GetSnapshot()
+ {
+ return new PriorityApiSnapshot(
+ ApiVersion,
+ GetMaxPriority(),
+ GetDefaultEnabledPriority());
+ }
+
+ ///
+ /// Safe reflection-friendly wrapper that never throws.
+ /// Returns false only if the API could not produce a valid snapshot.
+ ///
+ public static bool TryGetSnapshot(out PriorityApiSnapshot snapshot)
+ {
+ try
+ {
+ snapshot = GetSnapshot();
+ return true;
+ }
+ catch
+ {
+ snapshot = new PriorityApiSnapshot(ApiVersion, Math.Max(1, DefaultSettings.maxPriority), 3);
+ return false;
+ }
+ }
+
+ private static int Clamp(int value, int min, int max)
+ {
+ if (value < min) return min;
+ if (value > max) return max;
+ return value;
+ }
+ }
+}
diff --git a/Source/API/PriorityApi.md b/Source/API/PriorityApi.md
new file mode 100644
index 0000000..d62cf46
--- /dev/null
+++ b/Source/API/PriorityApi.md
@@ -0,0 +1,45 @@
+# Better Work Tab Priority API
+
+This API exists so other mods can read Better Work Tab's extended priority settings without hardcoding the vanilla max priority of `4`.
+
+## Goals
+
+- No hard dependency required
+- Stable reflection target
+- Small read-only surface
+- Versioned for future extension
+
+## Reflection Contract
+
+- Assembly name: `Better Work Tab`
+- Type name: `Better_Work_Tab.API.PriorityApi`
+- API version field: `ApiVersion`
+
+## Public Methods
+
+- `GetMaxPriority()`
+- `GetDefaultEnabledPriority()`
+- `MapPriorityToVanillaDisplay(int priority)`
+- `IsDisabledPriority(int priority)`
+- `GetSnapshot()`
+- `TryGetSnapshot(out PriorityApiSnapshot snapshot)`
+
+## Recommended Integration
+
+If you do not want a compile-time reference to Better Work Tab:
+
+1. Find the loaded assembly named `Better Work Tab`
+2. Resolve `Better_Work_Tab.API.PriorityApi`
+3. Prefer calling `GetSnapshot()` or `TryGetSnapshot(...)`
+
+`GetSnapshot()` returns a versioned struct with:
+
+- `ApiVersion`
+- `MaxPriority`
+- `DefaultEnabledPriority`
+
+## Notes
+
+- `0` means disabled
+- Higher numbers are lower priority
+- `MapPriorityToVanillaDisplay(...)` intentionally compresses extended priorities back into vanilla `1..4` display buckets for compatibility-oriented UI
diff --git a/Source/Better Work Tab.csproj b/Source/Better Work Tab.csproj
index 2c0f28b..bc0aad6 100644
--- a/Source/Better Work Tab.csproj
+++ b/Source/Better Work Tab.csproj
@@ -49,6 +49,7 @@
+
@@ -110,7 +111,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -204,6 +225,7 @@
+
diff --git a/Source/BetterWorkTabSettings.cs b/Source/BetterWorkTabSettings.cs
index 018a772..12c5899 100644
--- a/Source/BetterWorkTabSettings.cs
+++ b/Source/BetterWorkTabSettings.cs
@@ -204,11 +204,11 @@ public enum ColorScheme { RimWorldDefault, Colorblind_Deuteranopia, Colorblind_P
new WorkAssignmentParameters("Always Assigns", 3, isNaturalAlwaysAssign: true),
}, resetBeforeApplying: true, isDefault: true),
- new WorkAssignmentRuleset("Vanilla New Pawn", new List()
- {
- new WorkAssignmentParameters("Top 6", 3, isTopXSkill: 6),
- new WorkAssignmentParameters("Always Assigns", 3, isNaturalAlwaysAssign: true),
- }, resetBeforeApplying: false, isDefault: true),
+ //new WorkAssignmentRuleset("Vanilla New Pawn", new List()
+ //{
+ // new WorkAssignmentParameters("Top 6", 3, isTopXSkill: 6),
+ // new WorkAssignmentParameters("Always Assigns", 3, isNaturalAlwaysAssign: true),
+ //}, resetBeforeApplying: false, isDefault: true),
new WorkAssignmentRuleset("BWT Default", new List()
{
@@ -239,6 +239,11 @@ public enum ColorScheme { RimWorldDefault, Colorblind_Deuteranopia, Colorblind_P
// UI mode settings for work tab visibility
public static BetterWorkTabSettings.ShowUIMode ShowUIMode_ShowSmallSkillNumbers = BetterWorkTabSettings.ShowUIMode.Unshifted;
public static BetterWorkTabSettings.ShowUIMode ShowUIMode_ShowPawnForSkillSquare = BetterWorkTabSettings.ShowUIMode.Shifted;
+
+ public static int maxPriority = 9;
+ public static int priorityColorPercentage_Green = 10;
+ public static int priorityColorPercentage_Yellow = 50;
+ public static int priorityColorPercentage_Tan = 75;
}
// Contains all configurable settings for Better Work Tab mod
@@ -416,11 +421,20 @@ public Color Color_SimilarWorktypeMouseOver
public bool mpSyncRulesets = true;
public enum MpConflictMode { PlayerPriority, HostPriority, AskPlayer }
public MpConflictMode mpConflictMode = MpConflictMode.PlayerPriority;
+
+ // Max Priority Int Settings
+ public const int MAX_PRIORITY_HARD_LIMIT = 99;
+
+ public int maxPriorityInt = DefaultSettings.maxPriority;
+ public int priorityColorPercentage_Green = 10;
+ public int priorityColorPercentage_Yellow = 50;
+ public int priorityColorPercentage_Tan = 75;
+
public bool mpShowOtherPlayersHover = DefaultSettings.mpShowOtherPlayersHover;
public bool mpAllowOthersToRequestLayout = DefaultSettings.mpAllowOthersToRequestLayout;
public bool mpAllowPresenceBroadcast = DefaultSettings.mpAllowPresenceBroadcast;
public bool mpShowLinkedIndicator = DefaultSettings.mpShowLinkedIndicator;
-
+
///
/// Unique identifier for this BWT installation in multiplayer
/// Auto-generated but user-configurable
@@ -652,9 +666,69 @@ public override void ExposeData()
Scribe_Values.Look(ref cjkVerticalKerning, "cjkVerticalKerning", 0.75f);
Scribe_Values.Look(ref angledHeaderColor, "angledHeaderColor", DefaultSettings.Color_AngledHeaderText);
Scribe_Values.Look(ref autoEnableManualPriorities, "autoEnableManualPriorities", DefaultSettings.autoEnableManualPriorities);
+ Scribe_Values.Look(ref maxPriorityInt, "maxPriorityInt", DefaultSettings.maxPriority);
+ Scribe_Values.Look(ref priorityColorPercentage_Green, "priorityColorPercentage_Green", DefaultSettings.priorityColorPercentage_Green);
+ Scribe_Values.Look(ref priorityColorPercentage_Yellow , "priorityColorPercentage_Yellow", DefaultSettings.priorityColorPercentage_Yellow );
+ Scribe_Values.Look(ref priorityColorPercentage_Tan , "priorityColorPercentage_Tan", DefaultSettings.priorityColorPercentage_Tan );
if (hiddenWorktypes == null) hiddenWorktypes = new List();
+ // Colors
+ Scribe_Values.Look(ref Color_CursorHighlight, "Color_CursorHighlight", DefaultSettings.Color_CursorHighlight);
+ Scribe_Values.Look(ref Color_FloatMenuHighlight, "Color_FloatMenuHighlight", DefaultSettings.Color_FloatMenuHighlight);
+ Scribe_Values.Look(ref Color_CustomMouseHighlight, "Color_CustomMouseHighlight", DefaultSettings.Color_CustomMouseHighlight);
+ Scribe_Values.Look(ref Color_CustomSimilarWorktypeHighlight, "Color_CustomSimilarWorktypeHighlight", DefaultSettings.Color_CustomSimilarWorktypeHighlight);
+ Scribe_Values.Look(ref Color_IncapableBecauseOfCapacities, "Color_IncapableBecauseOfCapacities", DefaultSettings.Color_IncapableBecauseOfCapacities);
+ Scribe_Values.Look(ref Color_BestPawnForSkillSquare, "Color_BestPawnForSkillSquare", DefaultSettings.Color_BestPawnForSkillSquare);
+ Scribe_Values.Look(ref Color_VeryLowSkill, "Color_VeryLowSkill", DefaultSettings.Color_VeryLowSkill);
+ Scribe_Values.Look(ref Color_LowSkill, "Color_LowSkill", DefaultSettings.Color_LowSkill);
+ Scribe_Values.Look(ref Color_GoodLowSkill, "Color_GoodLowSkill", DefaultSettings.Color_GoodLowSkill);
+ Scribe_Values.Look(ref Color_ExcellentSkill, "Color_ExcellentSkill", DefaultSettings.Color_ExcellentSkill);
+ Scribe_Values.Look(ref Color_RowHoverHighlight, "Color_RowHoverHighlight", DefaultSettings.Color_RowHoverHighlight);
+ Scribe_Values.Look(ref Color_ColumnHoverHighlight, "Color_ColumnHoverHighlight", DefaultSettings.Color_ColumnHoverHighlight);
+ Scribe_Values.Look(ref Color_SelectedPawnHighlight, "Color_SelectedPawnHighlight", DefaultSettings.Color_SelectedPawnHighlight);
+ Scribe_Values.Look(ref Color_HeaderText, "Color_HeaderText", DefaultSettings.Color_HeaderText);
+ Scribe_Values.Look(ref Color_DividerText, "Color_DividerText", DefaultSettings.Color_DividerText);
+ Scribe_Values.Look(ref Color_Borders, "Color_Borders", DefaultSettings.Color_Borders);
+
+ // UI modes
+ Scribe_Values.Look(ref ShowUIMode_ShowSmallSkillNumbers, "ShowUIMode_ShowSmallSkillNumbers", DefaultSettings.ShowUIMode_ShowSmallSkillNumbers);
+ Scribe_Values.Look(ref ShowUIMode_ShowPawnForSkillSquare, "ShowUIMode_ShowPawnForSkillSquare", DefaultSettings.ShowUIMode_ShowPawnForSkillSquare);
+ Scribe_Values.Look(ref hoverEffectScope, "hoverEffectScope", DefaultSettings.hoverEffectScope);
+
+ // Future behavior templates
+ Scribe_Values.Look(ref confirmRulesetApplication, "confirmRulesetApplication", DefaultSettings.confirmRulesetApplication);
+ Scribe_Values.Look(ref dragStartThreshold, "dragStartThreshold", DefaultSettings.dragStartThreshold);
+ Scribe_Values.Look(ref scrollSpeed, "scrollSpeed", DefaultSettings.scrollSpeed);
+ Scribe_Values.Look(ref showAutoAssignConfirmation, "showAutoAssignConfirmation", DefaultSettings.showAutoAssignConfirmation);
+ Scribe_Values.Look(ref resetWorkBeforeAutoAssign, "resetWorkBeforeAutoAssign", DefaultSettings.resetWorkBeforeAutoAssign);
+ Scribe_Values.Look(ref showAutoAssignVisualFeedback, "showAutoAssignVisualFeedback", DefaultSettings.showAutoAssignVisualFeedback);
+ Scribe_Values.Look(ref defaultAutoAssignRuleset, "defaultAutoAssignRuleset", "BWT Default");
+ Scribe_Values.Look(ref showWorkloadButtonFooter, "showWorkloadButtonFooter", DefaultSettings.showWorkloadButtonFooter);
+ Scribe_Values.Look(ref enableWorkloadSaving, "enableWorkloadSaving", DefaultSettings.enableWorkloadSaving);
+ Scribe_Values.Look(ref enableWorkloadLoading, "enableWorkloadLoading", DefaultSettings.enableWorkloadLoading);
+ Scribe_Values.Look(ref persistDividersInWorkloads, "persistDividersInWorkloads", DefaultSettings.persistDividersInWorkloads);
+ Scribe_Values.Look(ref alwaysShowConditionEditors, "alwaysShowConditionEditors", DefaultSettings.alwaysShowConditionEditors);
+ Scribe_Values.Look(ref cacheBedCounts, "cacheBedCounts", true);
+ Scribe_Values.Look(ref cacheSkillLevels, "cacheSkillLevels", true);
+ Scribe_Values.Look(ref cacheRowDescriptors, "cacheRowDescriptors", true);
+ Scribe_Values.Look(ref cacheIncapabilityChecks, "cacheIncapabilityChecks", true);
+ Scribe_Values.Look(ref useElementPooling, "useElementPooling", true);
+ Scribe_Values.Look(ref viewportCulling, "viewportCulling", true);
+ Scribe_Values.Look(ref enableProfiler, "enableProfiler", false);
+ Scribe_Values.Look(ref logDebugToFile, "logDebugToFile", false);
+ Scribe_Values.Look(ref mpSyncColumnOrder, "mpSyncColumnOrder", true);
+ Scribe_Values.Look(ref mpSyncWorkloads, "mpSyncWorkloads", true);
+ Scribe_Values.Look(ref mpSyncRulesets, "mpSyncRulesets", true);
+ Scribe_Values.Look(ref mpConflictMode, "mpConflictMode", MpConflictMode.PlayerPriority);
+ Scribe_Values.Look(ref rulesetViewMode, "rulesetViewMode", RulesetViewMode.Regular);
+
+ //Scribe_Values.Look(ref maxPriorityInt, "maxPriorityInt", 4);
+
+ // Divider settings
+ Scribe_Values.Look(ref dividerHeight, "dividerHeight", DefaultSettings.dividerHeight);
+
+ // Load rulesets from save file
Scribe_Collections.Look(ref SavedRulesets, "SavedRulesets", LookMode.Deep);
// Reinitialize rulesets after load (restores defaults if missing)
diff --git a/Source/Features/Patches/CustomWorkBoxDrawer.cs b/Source/Features/Patches/CustomWorkBoxDrawer.cs
index e9a8668..b36b759 100644
--- a/Source/Features/Patches/CustomWorkBoxDrawer.cs
+++ b/Source/Features/Patches/CustomWorkBoxDrawer.cs
@@ -1,3 +1,4 @@
+using Better_Work_Tab.Features.RaisedPriorityMaximum;
using RimWorld;
using UnityEngine;
using Verse;
@@ -8,6 +9,12 @@ namespace Better_Work_Tab.Patches
// Custom work box drawer that preserves all vanilla visuals except priority number.
public static class CustomWorkBoxDrawer
{
+ private const float CompactPriorityPaddingX = 1f;
+ private const float CompactPriorityPaddingY = -1f;
+ private const float CompactPriorityHeight = 12f;
+ private const float CompactPriorityBaseWidth = 14f;
+ private const float CompactPriorityExtraWidthPerDigit = 6f;
+
///
/// Draws a work box with vanilla visuals (background, passion flames, incapable tint)
/// but WITHOUT the priority number or click handling.
@@ -48,5 +55,42 @@ public static void DrawWorkBoxForSkillOverlay(float x, float y, Pawn p, WorkType
}
}
+
+ ///
+ /// Draws a compact priority label for custom work cell views.
+ ///
+ public static void DrawCompactPriority(Rect cellRect, int priority)
+ {
+ if (priority <= 0)
+ {
+ return;
+ }
+
+ string label = priority.ToString();
+ float width = CompactPriorityBaseWidth + Mathf.Max(0, label.Length - 1) * CompactPriorityExtraWidthPerDigit;
+ Rect labelRect = new Rect(
+ cellRect.x + CompactPriorityPaddingX,
+ cellRect.y + CompactPriorityPaddingY,
+ width,
+ CompactPriorityHeight);
+
+ DrawPriorityLabel(labelRect, label, priority, GameFont.Tiny, TextAnchor.MiddleCenter);
+ }
+
+ private static void DrawPriorityLabel(Rect labelRect, string label, int priority, GameFont font, TextAnchor anchor)
+ {
+ var oldFont = Text.Font;
+ var oldAnchor = Text.Anchor;
+ var oldColor = GUI.color;
+
+ Text.Font = font;
+ Text.Anchor = anchor;
+ GUI.color = MaxPriorityLogic.GetPriorityColor(priority);
+ Widgets.Label(labelRect, label);
+
+ GUI.color = oldColor;
+ Text.Font = oldFont;
+ Text.Anchor = oldAnchor;
+ }
}
-}
\ No newline at end of file
+}
diff --git a/Source/Features/Patches/MaxPriorityPatches.cs b/Source/Features/Patches/MaxPriorityPatches.cs
new file mode 100644
index 0000000..8d35af8
--- /dev/null
+++ b/Source/Features/Patches/MaxPriorityPatches.cs
@@ -0,0 +1,340 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Reflection.Emit;
+using HarmonyLib;
+using ModAPI.Harmony;
+using RimWorld;
+using UnityEngine;
+using Verse;
+
+namespace Better_Work_Tab.Features.RaisedPriorityMaximum
+{
+ ///
+ /// Shared method handles used by the max-priority patches.
+ ///
+ internal static class PriorityIl
+ {
+ internal static readonly MethodInfo GetPriority = AccessTools.Method(
+ typeof(Pawn_WorkSettings),
+ nameof(Pawn_WorkSettings.GetPriority),
+ new[] { typeof(WorkTypeDef) });
+
+ internal static readonly MethodInfo GetTooltipPriority = AccessTools.Method(
+ typeof(MaxPriorityLogic),
+ nameof(MaxPriorityLogic.GetTooltipPriority),
+ new[] { typeof(Pawn_WorkSettings), typeof(WorkTypeDef) });
+
+ internal static readonly MethodInfo GetMaxPriority = AccessTools.Method(
+ typeof(MaxPriorityLogic),
+ nameof(MaxPriorityLogic.GetMaxPriority));
+ }
+
+ ///
+ /// Centralized rules for extended manual priorities.
+ ///
+ internal static class MaxPriorityLogic
+ {
+ ///
+ /// Returns the configured upper bound for manual priorities.
+ ///
+ internal static int GetMaxPriority()
+ {
+ return Math.Max(1, BetterWorkTabMod.Settings.maxPriorityInt);
+ }
+
+ ///
+ /// Returns the default priority used when enabling a work type outside manual priorities.
+ /// Uses vanilla's normal priority as a baseline, clamped to the configured range.
+ ///
+ internal static int GetDefaultEnabledPriority()
+ {
+ return Mathf.Clamp(3, 1, GetMaxPriority());
+ }
+
+ ///
+ /// Maps extended priorities back into RimWorld's tooltip display range.
+ ///
+ internal static int MapPriorityToVanillaDisplay(int priority)
+ {
+ if (priority <= 0)
+ {
+ return 0;
+ }
+
+ int maxPriority = GetMaxPriority();
+ if (maxPriority <= 1)
+ {
+ return 1;
+ }
+
+ return Mathf.Clamp((int)Math.Round(Spine.Utils.SpineUtils.Remap(priority, 1, maxPriority, 1, 4)), 1, 4);
+ }
+
+ ///
+ /// Supplies the tooltip priority after remapping it into the vanilla display range.
+ ///
+ internal static int GetTooltipPriority(Pawn_WorkSettings workSettings, WorkTypeDef workType)
+ {
+ if (workSettings == null || workType == null)
+ {
+ return 0;
+ }
+
+ return MapPriorityToVanillaDisplay(workSettings.GetPriority(workType));
+ }
+
+ ///
+ /// Returns the color used for a manual priority value.
+ ///
+ internal static Color GetPriorityColor(int priority)
+ {
+ if (priority <= 0)
+ {
+ return Color.grey;
+ }
+
+ int percentage = (int)(((float)priority / GetMaxPriority()) * 100f);
+
+ if (percentage < BetterWorkTabMod.Settings.priorityColorPercentage_Green)
+ {
+ return new Color(0f, 1f, 0f);
+ }
+
+ if (percentage < BetterWorkTabMod.Settings.priorityColorPercentage_Yellow)
+ {
+ return new Color(1f, 0.9f, 0.5f);
+ }
+
+ if (percentage < BetterWorkTabMod.Settings.priorityColorPercentage_Tan)
+ {
+ return new Color(0.8f, 0.7f, 0.5f);
+ }
+
+ return new Color(0.74f, 0.74f, 0.74f);
+ }
+ }
+
+ ///
+ /// Locates the specific IL instructions that enforce RimWorld's vanilla max priority.
+ ///
+ internal static class PriorityTranspilerPatterns
+ {
+ ///
+ /// Finds the constant used by RimWorld's decrement wraparound path.
+ /// The matched sequence is:
+ /// GetPriority, subtract 1, store to a local, test that local against 0, and if it is negative load 4.
+ /// The returned index is the final 4 load, which is the instruction replaced with GetMaxPriority().
+ ///
+ internal static int FindPriorityWrapUnderflowIndex(List codes, int startIndex)
+ {
+ return FindPattern(
+ codes,
+ startIndex,
+ (list, i) => i + 7 < list.Count &&
+ list[i].Calls(PriorityIl.GetPriority) &&
+ list[i + 1].LoadsConstant(1) &&
+ list[i + 2].opcode == OpCodes.Sub &&
+ IsStoreLocal(list[i + 3]) &&
+ IsLoadLocal(list[i + 4]) &&
+ list[i + 5].LoadsConstant(0) &&
+ IsBranch(list[i + 6], OpCodes.Bge, OpCodes.Bge_S) &&
+ list[i + 7].LoadsConstant(4),
+ i => i + 7);
+ }
+
+ ///
+ /// Finds the constant used by RimWorld's increment wraparound path.
+ /// The matched sequence is:
+ /// GetPriority, add 1, store to a local, compare that local against 4, and if it exceeds the limit load 0.
+ /// The returned index is the comparison's 4 load, which is the instruction replaced with GetMaxPriority().
+ ///
+ internal static int FindPriorityWrapOverflowIndex(List codes, int startIndex)
+ {
+ return FindPattern(
+ codes,
+ startIndex,
+ (list, i) => i + 7 < list.Count &&
+ list[i].Calls(PriorityIl.GetPriority) &&
+ list[i + 1].LoadsConstant(1) &&
+ list[i + 2].opcode == OpCodes.Add &&
+ IsStoreLocal(list[i + 3]) &&
+ IsLoadLocal(list[i + 4]) &&
+ list[i + 5].LoadsConstant(4) &&
+ IsBranch(list[i + 6], OpCodes.Ble, OpCodes.Ble_S) &&
+ list[i + 7].LoadsConstant(0),
+ i => i + 5);
+ }
+
+ ///
+ /// Finds the upper-bound check inside Pawn_WorkSettings.SetPriority.
+ ///
+ internal static int FindSetPriorityUpperBoundIndex(List codes)
+ {
+ return FindPattern(
+ codes,
+ 0,
+ (list, i) => i + 5 < list.Count &&
+ IsLoadArgument(list[i], 2) &&
+ list[i + 1].LoadsConstant(0) &&
+ IsBranch(list[i + 2], OpCodes.Blt, OpCodes.Blt_S) &&
+ IsLoadArgument(list[i + 3], 2) &&
+ list[i + 4].LoadsConstant(4) &&
+ IsBranch(list[i + 5], OpCodes.Ble, OpCodes.Ble_S),
+ i => i + 4);
+ }
+
+ ///
+ /// Replaces a vanilla constant load with a call that returns the configured max priority.
+ ///
+ internal static void ReplaceWithMaxPriorityCall(List codes, int index)
+ {
+ codes[index] = new CodeInstruction(OpCodes.Call, PriorityIl.GetMaxPriority);
+ }
+
+ private static int FindPattern(List codes, int startIndex, Func, int, bool> predicate, Func resultSelector)
+ {
+ for (int i = Math.Max(0, startIndex); i < codes.Count; i++)
+ {
+ if (predicate(codes, i))
+ {
+ return resultSelector(i);
+ }
+ }
+
+ return -1;
+ }
+
+ private static bool IsBranch(CodeInstruction instruction, OpCode longForm, OpCode shortForm)
+ {
+ return instruction.opcode == longForm || instruction.opcode == shortForm;
+ }
+
+ private static bool IsLoadLocal(CodeInstruction instruction)
+ {
+ return instruction.opcode.Name.StartsWith("ldloc", StringComparison.Ordinal);
+ }
+
+ private static bool IsStoreLocal(CodeInstruction instruction)
+ {
+ return instruction.opcode.Name.StartsWith("stloc", StringComparison.Ordinal);
+ }
+
+ private static bool IsLoadArgument(CodeInstruction instruction, int argumentIndex)
+ {
+ return argumentIndex switch
+ {
+ 0 => instruction.opcode == OpCodes.Ldarg_0,
+ 1 => instruction.opcode == OpCodes.Ldarg_1,
+ 2 => instruction.opcode == OpCodes.Ldarg_2 ||
+ (instruction.opcode == OpCodes.Ldarg_S &&
+ ((instruction.operand is byte byteIndex && byteIndex == 2) ||
+ (instruction.operand is ushort shortIndex && shortIndex == 2))),
+ 3 => instruction.opcode == OpCodes.Ldarg_3,
+ _ => false
+ };
+ }
+ }
+
+ [HarmonyPatch(typeof(WidgetsWork), nameof(WidgetsWork.ColorOfPriority))]
+ internal static class Patch_WidgetsWork_ColorOfPriority
+ {
+ ///
+ /// Extends RimWorld's priority color calculation beyond the vanilla 1..4 range.
+ ///
+ [HarmonyPostfix]
+ private static void Postfix(ref Color __result, int prio)
+ {
+ __result = MaxPriorityLogic.GetPriorityColor(prio);
+ }
+ }
+
+ [HarmonyPatch(typeof(WidgetsWork), nameof(WidgetsWork.TipForPawnWorker))]
+ internal static class Patch_WidgetsWork_TipForPawnWorker
+ {
+ ///
+ /// Redirects the tooltip priority lookup so tooltips keep using RimWorld's translated
+ /// 1..4 labels even when the stored priority exceeds 4.
+ ///
+ [HarmonyTranspiler]
+ private static IEnumerable Transpiler(IEnumerable instructions, MethodBase original)
+ {
+ return FluentTranspiler.Execute(instructions, original, null, t =>
+ {
+ t.MatchCall(PriorityIl.GetPriority)
+ .AssertValid()
+ .ReplaceWithCall(typeof(MaxPriorityLogic), nameof(MaxPriorityLogic.GetTooltipPriority), new[] { typeof(Pawn_WorkSettings), typeof(WorkTypeDef) });
+ });
+ }
+ }
+
+ [HarmonyPatch(typeof(WidgetsWork), nameof(WidgetsWork.DrawWorkBoxFor))]
+ internal static class Patch_WidgetsWork_DrawWorkBoxFor
+ {
+ ///
+ /// Replaces the two wraparound constants used by the work-cell click handlers.
+ ///
+ [HarmonyTranspiler]
+ private static IEnumerable Transpiler(IEnumerable instructions, MethodBase original)
+ {
+ var codes = new List(instructions);
+ int leftWrapIndex = PriorityTranspilerPatterns.FindPriorityWrapUnderflowIndex(codes, 0);
+ int rightWrapIndex = PriorityTranspilerPatterns.FindPriorityWrapOverflowIndex(codes, leftWrapIndex + 1);
+
+ if (leftWrapIndex < 0 || rightWrapIndex < 0)
+ {
+ throw new InvalidOperationException($"Unable to locate work-box priority wrap checks in {original?.DeclaringType?.Name}.{original?.Name}.");
+ }
+
+ PriorityTranspilerPatterns.ReplaceWithMaxPriorityCall(codes, leftWrapIndex);
+ PriorityTranspilerPatterns.ReplaceWithMaxPriorityCall(codes, rightWrapIndex);
+ return codes;
+ }
+ }
+
+ [HarmonyPatch(typeof(PawnColumnWorker_WorkPriority), nameof(PawnColumnWorker_WorkPriority.HeaderClicked))]
+ internal static class Patch_PawnColumnWorker_WorkPriority_HeaderClicked
+ {
+ ///
+ /// Replaces the two wraparound constants used by the header bulk-edit controls.
+ ///
+ [HarmonyTranspiler]
+ private static IEnumerable Transpiler(IEnumerable instructions, MethodBase original)
+ {
+ var codes = new List(instructions);
+ int leftWrapIndex = PriorityTranspilerPatterns.FindPriorityWrapUnderflowIndex(codes, 0);
+ int rightWrapIndex = PriorityTranspilerPatterns.FindPriorityWrapOverflowIndex(codes, leftWrapIndex + 1);
+
+ if (leftWrapIndex < 0 || rightWrapIndex < 0)
+ {
+ throw new InvalidOperationException($"Unable to locate header priority wrap checks in {original?.DeclaringType?.Name}.{original?.Name}.");
+ }
+
+ PriorityTranspilerPatterns.ReplaceWithMaxPriorityCall(codes, leftWrapIndex);
+ PriorityTranspilerPatterns.ReplaceWithMaxPriorityCall(codes, rightWrapIndex);
+ return codes;
+ }
+ }
+
+ [HarmonyPatch(typeof(Pawn_WorkSettings), nameof(Pawn_WorkSettings.SetPriority))]
+ internal static class Patch_Pawn_WorkSettings_SetPriority
+ {
+ ///
+ /// Replaces RimWorld's validation ceiling so values above 4 remain valid.
+ ///
+ [HarmonyTranspiler]
+ private static IEnumerable Transpiler(IEnumerable instructions, MethodBase original)
+ {
+ var codes = new List(instructions);
+ int upperBoundIndex = PriorityTranspilerPatterns.FindSetPriorityUpperBoundIndex(codes);
+
+ if (upperBoundIndex < 0)
+ {
+ throw new InvalidOperationException($"Unable to locate the SetPriority upper-bound check in {original?.DeclaringType?.Name}.{original?.Name}.");
+ }
+
+ PriorityTranspilerPatterns.ReplaceWithMaxPriorityCall(codes, upperBoundIndex);
+ return codes;
+ }
+ }
+}
diff --git a/Source/Features/Patches/Patch_Pawn_WorkSettings_SetPriority.cs b/Source/Features/Patches/Patch_Pawn_WorkSettings_SetPriority.cs
index 73aba61..2234656 100644
--- a/Source/Features/Patches/Patch_Pawn_WorkSettings_SetPriority.cs
+++ b/Source/Features/Patches/Patch_Pawn_WorkSettings_SetPriority.cs
@@ -1,6 +1,5 @@
using HarmonyLib;
using RimWorld;
-using Verse;
namespace Better_Work_Tab.Features.Patches
{
@@ -14,13 +13,11 @@ public static class Patch_Pawn_WorkSettings_SetPriority
[HarmonyPrefix]
public static bool Prefix()
{
- // Block priority changes if local player is currently dragging a header
if (BetterWorkTabLocalState.IsHeaderDragging)
{
- return false; // Skip the original method
+ return false;
}
-
- return true; // Allow the original method to run
+ return true;
}
}
}
diff --git a/Source/Features/Patches/Patch_WorkPriority_DoCell_Unified.cs b/Source/Features/Patches/Patch_WorkPriority_DoCell_Unified.cs
index cdbdad7..5606f5a 100644
--- a/Source/Features/Patches/Patch_WorkPriority_DoCell_Unified.cs
+++ b/Source/Features/Patches/Patch_WorkPriority_DoCell_Unified.cs
@@ -9,6 +9,7 @@
using System.Linq;
using UnityEngine;
using Verse;
+using Verse.Sound;
namespace Better_Work_Tab.Patches
{
@@ -78,7 +79,7 @@ private static Rect GetLabelRect(PawnColumnWorker_WorkPriority worker, Rect head
[HarmonyPatch(typeof(PawnColumnWorker_WorkPriority), nameof(PawnColumnWorker_WorkPriority.DoCell))]
public static class Patch_WorkPriority_DoCell_Unified
{
- // === FRAME-LEVEL STATE ===
+ // Frame state
private static int _lastCachedFrame = -1;
private static bool _cachedShiftHeld = false;
private static bool _cachedFeatureEnabled = false;
@@ -89,7 +90,7 @@ public static class Patch_WorkPriority_DoCell_Unified
private static WorkTypeDef _columnHoveredWorkType;
private static int _columnHoveredFrame = -1;
- // === CACHES ===
+ // Cached per-frame and per-worktype values
private static readonly Dictionary _skillCache = new Dictionary(1024);
private static readonly Dictionary _skillCacheTimestamps = new Dictionary(1024);
private static readonly Dictionary _incapableCache = new Dictionary(1024);
@@ -98,7 +99,7 @@ public static class Patch_WorkPriority_DoCell_Unified
private static readonly Dictionary _bestPawnCacheTimestamps = new Dictionary(64);
private static readonly Dictionary _colorCache = new Dictionary(21);
- // Constants
+ // Layout and cache settings
private const int SkillCacheFrameValidity = 60;
private const int IncapableCacheFrameValidity = 120;
private const int BestPawnCacheFrameValidity = 60;
@@ -142,7 +143,8 @@ public static bool Prefix(
return true;
WorkTypeDef workType = __instance.def.workType;
- if (workType == null) return true;
+ if (workType == null)
+ return true;
UpdateFrameCache();
@@ -150,38 +152,35 @@ public static bool Prefix(
if (BetterWorkTabMod.Settings.enableScrollWheelPriority && Event.current.type == EventType.ScrollWheel && Mouse.IsOver(rect))
{
int currentPriority = pawn.workSettings.GetPriority(workType);
- int delta = Event.current.delta.y > 0 ? -1 : 1; // Scroll up = increase priority (closer to 1), Scroll down = decrease
- // TODO: Will need to update this when custom priorities are added
+ int delta = Event.current.delta.y > 0 ? -1 : 1;
if (Find.PlaySettings.useWorkPriorities)
{
- // Manual priorities: 1-4, 0 is disabled.
- // Note: RimWorld priorities are 1 (Highest) to 4 (Lowest). 0 is Off.
- // Scroll Up (delta +1): 0 -> 4 -> 3 -> 2 -> 1
- // Scroll Down (delta -1): 1 -> 2 -> 3 -> 4 -> 0
int nextPriority = currentPriority;
- if (delta > 0) // Increase (1 is high, 4 is low)
+ if (delta > 0)
{
- if (currentPriority == 0) nextPriority = 4;
+ if (currentPriority == 0) nextPriority = BetterWorkTabMod.Settings.maxPriorityInt;
else if (currentPriority > 1) nextPriority = currentPriority - 1;
}
- else // Decrease
+ else
{
- if (currentPriority == 4) nextPriority = 0;
+ if (currentPriority == BetterWorkTabMod.Settings.maxPriorityInt) nextPriority = 0;
else if (currentPriority > 0) nextPriority = currentPriority + 1;
}
if (nextPriority != currentPriority)
{
pawn.workSettings.SetPriority(workType, nextPriority);
+ SoundDefOf.DragSlider.PlayOneShotOnCamera();
}
}
else
{
- // Checkbox mode: 0 or 3
int nextPriority = (currentPriority > 0) ? 0 : 3;
if (nextPriority != currentPriority)
{
pawn.workSettings.SetPriority(workType, nextPriority);
+ SoundDefOf.DragSlider.PlayOneShotOnCamera();
+
}
}
Event.current.Use();
@@ -201,7 +200,7 @@ public static bool Prefix(
return true;
if (workType.relevantSkills == null || workType.relevantSkills.Count == 0)
- return false; // Skip vanilla drawing if no relevant skills, to draw nothing or custom
+ return true;
// Track column hover state only if hover cell overlay is enabled
bool hoveringCell = Mouse.IsOver(rect);
@@ -233,7 +232,6 @@ public static bool Prefix(
if (columnHovered)
{
- // For column-wide hover, allow vanilla draw in Standard so priority numbers render across the column.
if (_cachedHoverMode == BetterWorkTabSettings.SkillViewHoverMode.Standard)
{
return true;
@@ -241,8 +239,6 @@ public static bool Prefix(
return false;
}
- // If NOT hovering, return FALSE to skip Vanilla.
- // Postfix will draw the Big Skill Number and static background.
return false;
}
@@ -267,21 +263,20 @@ public static void Postfix(
if (GetIsIncapable(pawn, workType))
return;
- if (workType.relevantSkills == null || workType.relevantSkills.Count == 0)
- return;
-
// Draw best pawn outline - this uses the ShowUIMode setting to determine when to show
// (can be Always, Shifted, Unshifted, or Never)
DrawBestPawnOutlineIfNeeded(__instance, rect, pawn, table, workType);
- // === Skill Overlay Section (only when shift is held) ===
- // Basic feature check: if skill overlay is disabled or shift not held, skip skill overlay
if (!_cachedFeatureEnabled || !_cachedShiftHeld)
return;
if (Patch_WorkPriority_DoHeader_HoverTracker.HoveredHeaderWorkType == workType)
return;
+ if (workType.relevantSkills == null || workType.relevantSkills.Count == 0)
+ return;
+
+ int priority = pawn.workSettings.GetPriority(workType);
int skillLevel = GetSkillLevel(pawn, workType);
bool hoveringCell = Mouse.IsOver(rect);
@@ -299,35 +294,32 @@ public static void Postfix(
bool drawBigSkill = true;
bool drawSmallSkill = false;
- bool drawSmallPriority = false;
bool showTinySkillNumbers = (BetterWorkTabMod.Settings?.enableSkillOverlayFeature ?? false) &&
ShouldShowUI(BetterWorkTabMod.Settings.ShowUIMode_ShowSmallSkillNumbers, _cachedUiState);
- // Apply hover-based transformations only if hover overlay is enabled
if (_cachedHoverCellOverlayEnabled && hovering)
{
if (_cachedHoverMode == BetterWorkTabSettings.SkillViewHoverMode.Standard)
{
- // Standard mode: hide big skill, show small skill on hover
drawBigSkill = false;
drawSmallSkill = true;
}
- else if (_cachedHoverMode == BetterWorkTabSettings.SkillViewHoverMode.SkillFocused)
- {
- // SkillFocused mode: keep big skill visible and show priority in the small-number slot
- drawSmallPriority = true;
- }
}
- // Always show tiny skill numbers if configured (regardless of hover state)
- if (showTinySkillNumbers && !drawSmallPriority)
+ if (showTinySkillNumbers)
{
drawSmallSkill = true;
}
+ CustomWorkBoxDrawer.DrawWorkBoxForSkillOverlay(boxXSkill, boxYSkill, pawn, workType, false);
+
+ if (priority > 0)
+ {
+ CustomWorkBoxDrawer.DrawCompactPriority(rect, priority);
+ }
+
if (drawBigSkill)
{
- CustomWorkBoxDrawer.DrawWorkBoxForSkillOverlay(boxXSkill, boxYSkill, pawn, workType, false);
DrawBigSkillNumber(boxRect, skillLevel);
}
@@ -335,22 +327,9 @@ public static void Postfix(
{
DrawSmallSkillNumbers(rect, skillLevel);
}
-
- if (drawSmallPriority)
- {
- int priority = pawn.workSettings.GetPriority(workType);
- if (priority > 0)
- {
- DrawSmallPriorityNumber(rect, priority);
- }
- }
}
- // ... [Rest of the caching and drawing helper methods remain exactly the same] ...
-
- // ==========================================================
- // OPTIMIZED CACHING LOGIC
- // ==========================================================
+ // Caching helpers
private static bool GetIsIncapable(Pawn p, WorkTypeDef work)
{
@@ -527,33 +506,6 @@ private static void DrawSmallSkillNumbers(Rect rect, int level)
Text.Anchor = oldAnchor;
}
- private static void DrawSmallPriorityNumber(Rect rect, int priority)
- {
- string prioStr = priority.ToString();
- float rightPadding = prioStr.Length >= 2 ? 0f : -3f;
- Rect prioRect = new Rect(
- rect.xMax - SkillBoxSize - rightPadding,
- rect.y + SmallSkillOffsetY,
- SkillBoxSize,
- SkillBoxSize);
-
- var oldFont = Text.Font;
- var oldAnchor = Text.Anchor;
- var oldColor = GUI.color;
-
- Text.Font = GameFont.Tiny;
- Text.Anchor = TextAnchor.MiddleCenter;
-
- // Use vanilla priority number color (white/gray like vanilla)
- GUI.color = new Color(0.9f, 0.9f, 0.9f);
-
- Widgets.Label(prioRect, priority.ToString());
-
- GUI.color = oldColor;
- Text.Font = oldFont;
- Text.Anchor = oldAnchor;
- }
-
///
/// Draws the best pawn outline if the settings allow it based on ShowUIMode.
/// This is called BEFORE the shift check so it can work with Always/Shifted/Unshifted modes.
diff --git a/Source/Features/Rules/WorkAssignmentRule.cs b/Source/Features/Rules/WorkAssignmentRule.cs
index b5ba360..a004828 100644
--- a/Source/Features/Rules/WorkAssignmentRule.cs
+++ b/Source/Features/Rules/WorkAssignmentRule.cs
@@ -96,7 +96,7 @@ public bool Apply(
// All checks passed – assign the priority
pawn.workSettings.SetPriority(
assigningWorktype,
- Mathf.Clamp(Parameters.Priority, 0, 4)
+ Mathf.Clamp(Parameters.Priority, 0, Mathf.Max(1, BetterWorkTabMod.Settings?.maxPriorityInt ?? 4))
);
return validationResult.ShouldSkipRemainingPawns;
diff --git a/Source/Features/Rules/WorkAssignmentRuleset.cs b/Source/Features/Rules/WorkAssignmentRuleset.cs
index fb40abc..0c42669 100644
--- a/Source/Features/Rules/WorkAssignmentRuleset.cs
+++ b/Source/Features/Rules/WorkAssignmentRuleset.cs
@@ -152,7 +152,11 @@ public void ApplyAutoAssignments()
if (eligiblePawns.Count > 0)
{
- eligiblePawns.RandomElement().workSettings.SetPriority(worktype, rule.Parameters.Priority);
+ int maxPriority = Mathf.Max(1, BetterWorkTabMod.Settings?.maxPriorityInt ?? 4);
+ eligiblePawns.RandomElement().workSettings.SetPriority(
+ worktype,
+ Mathf.Clamp(rule.Parameters.Priority, 0, maxPriority)
+ );
}
}
@@ -195,6 +199,8 @@ public WorkAssignmentRuleset Copy()
///
public void EnsurePriorityOrder(int maxPriority = 4)
{
+ maxPriority = Mathf.Max(1, maxPriority <= 0 ? BetterWorkTabMod.Settings?.maxPriorityInt ?? 4 : maxPriority);
+
if (PriorityOrder == null || PriorityOrder.Count == 0)
{
PriorityOrder = Enumerable.Range(1, maxPriority).ToList();
diff --git a/Source/Spine/Utils/SpineUtils.cs b/Source/Spine/Utils/SpineUtils.cs
new file mode 100644
index 0000000..7f4a815
--- /dev/null
+++ b/Source/Spine/Utils/SpineUtils.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Unity.Mathematics;
+using UnityEngine;
+
+namespace Spine.Utils
+{
+ internal class SpineUtils
+ {
+ ///
+ /// Given a value in range [from1, to1], remap it to the corresponding value in range [from2, to2]. Example: Remap(5, 0, 10, 0, 100) returns 50.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static float Remap(
+ float value,
+ float from1,
+ float to1,
+ float from2,
+ float to2)
+ {
+ //https://stackoverflow.com/questions/3451553/value-remapping
+ return from2 + (value - from1) * (to2 - from2) / (to1 - from1); //math.remap(from1, to1, from2, to2, value);
+ }
+ }
+}
diff --git a/Source/Spine/harmony/HarmonyBootstrap.cs b/Source/Spine/harmony/HarmonyBootstrap.cs
new file mode 100644
index 0000000..b523f99
--- /dev/null
+++ b/Source/Spine/harmony/HarmonyBootstrap.cs
@@ -0,0 +1,304 @@
+using System;
+using System.Reflection;
+using UnityEngine;
+using ModAPI.Core;
+using HarmonyLib;
+using Verse;
+
+namespace ModAPI.Harmony
+{
+ ///
+ /// Responsible for initializing Harmony and applying patches.
+ /// Redirects 0Harmony log to MMLog.
+ ///
+ [StaticConstructorOnStartup]
+ public static class HarmonyBootstrap
+ {
+ private static bool _installed = false;
+ private static GameObject _runnerGo;
+
+ static HarmonyBootstrap()
+ {
+ // Redirect Harmony's internal trace logs if needed
+ }
+
+ ///
+ /// Entry point for mod loader to start patching.
+ /// If Harmony DLL is not yet loaded by BepInEx or Doorstop,
+ /// starts a runner to periodically retry.
+ ///
+ public static void EnsurePatched()
+ {
+ if (_installed) return;
+
+ try
+ {
+ // Verify Harmony is available in current domain
+ var type = typeof(HarmonyLib.Harmony);
+ if (type == null)
+ {
+ MMLog.Write("HarmonyLib not found. Starting retry runner...");
+ StartRetryRunner();
+ return;
+ }
+
+ TryPatch();
+ }
+ catch (Exception ex)
+ {
+ MMLog.Write("Initial patch check failed: " + ex.Message);
+ StartRetryRunner();
+ }
+ }
+
+ private static void TryPatch()
+ {
+ if (_installed) return;
+ try
+ {
+ var asm = Assembly.GetExecutingAssembly();
+ var harmony = new HarmonyLib.Harmony("ShelteredModManager.ModAPI");
+
+ var opts = new ModAPI.Harmony.HarmonyUtil.PatchOptions
+ {
+ AllowDebugPatches = ReadManagerBool("EnableDebugPatches", false),
+ AllowDangerousPatches = ReadManagerBool("AllowDangerousPatches", false),
+ AllowStructReturns = ReadManagerBool("AllowStructReturns", false),
+ OnResult = (obj, reason) =>
+ {
+ if (reason != null)
+ {
+ var mb = obj as MemberInfo;
+ var name = mb != null ? (mb.DeclaringType != null ? mb.DeclaringType.Name + "." + mb.Name : mb.Name) : (obj?.ToString() ?? "");
+ MMLog.WriteDebug($"{name} -> {reason}");
+ }
+ }
+ };
+
+ var registryOptions = PatchRegistry.CreateManagerOptions(
+ opts,
+ asm.GetName().Name,
+ key => ReadManagerString(key, null));
+ PatchRegistry.ApplyAssembly(harmony, asm, registryOptions);
+
+ // Backward compatibility: patch ShelteredAPI too so Sheltered-specific
+ // implementations and adapters are activated alongside core ModAPI hooks.
+ var shelteredAssembly = ResolveShelteredApiAssembly();
+ if (shelteredAssembly != null && shelteredAssembly != asm)
+ {
+ string location = "";
+ try { location = shelteredAssembly.Location; } catch { }
+ MMLog.WriteInfo("HarmonyBootstrap: applying ShelteredAPI patches from "
+ + shelteredAssembly.GetName().Name + " v" + shelteredAssembly.GetName().Version
+ + " @" + location);
+ var shelteredRegistryOptions = PatchRegistry.CreateManagerOptions(
+ opts,
+ shelteredAssembly.GetName().Name,
+ key => ReadManagerString(key, null));
+ PatchRegistry.ApplyAssembly(harmony, shelteredAssembly, shelteredRegistryOptions);
+ TryInitializeShelteredApiCore(shelteredAssembly);
+ }
+ else
+ {
+ MMLog.WriteInfo("HarmonyBootstrap: ShelteredAPI assembly not found for patching (or same as ModAPI).");
+ }
+
+ LogPatchStatus("SettingsPCPanel.OnControlsButtonPressed", "SettingsPCPanel", "OnControlsButtonPressed");
+ LogPatchStatus("SettingsPCPanel.OnControlsButtonPressed_PAD", "SettingsPCPanel", "OnControlsButtonPressed_PAD");
+ LogPatchStatus("UIPanelManager.PushPanel(BasePanel)", "UIPanelManager", "PushPanel", "BasePanel");
+ LogPatchStatus("PlatformInput_PC.GetButtonDown(InputButton)", "PlatformInput_PC", "GetButtonDown", "PlatformInput+InputButton");
+ LogPatchStatus("PlatformInput_PC.GetButtonDown(MenuInputButton)", "PlatformInput_PC", "GetButtonDown", "PlatformInput+MenuInputButton");
+
+ // Explicitly verify UIPatches was discovered and patched
+ var uiPatches = asm.GetType("ModAPI.UI.UIPatches");
+ if (uiPatches != null)
+ {
+ MMLog.WriteDebug("Discovered UIPatches for verification.");
+ }
+
+ _installed = true;
+
+ MMLog.WriteDebug("ModAPI hooks patched");
+ if (_runnerGo != null) UnityEngine.Object.Destroy(_runnerGo);
+ }
+ catch (Exception ex)
+ {
+ MMLog.Write("patch attempt failed: " + ex.Message);
+ }
+ }
+
+ public static bool ReadManagerBool(string key, bool fallback)
+ {
+ string s = ReadManagerString(key, null);
+ if (s == null) return fallback;
+
+ bool b;
+ if (bool.TryParse(s, out b)) return b;
+
+ var lower = s.ToLowerInvariant();
+ if (lower == "1" || lower == "yes" || lower == "y" || lower == "on" || lower == "true") return true;
+ if (lower == "0" || lower == "no" || lower == "n" || lower == "off" || lower == "false") return false;
+
+ return fallback;
+ }
+
+ public static string ReadManagerString(string key, string fallback)
+ {
+ try
+ {
+ string gameRoot = System.IO.Path.GetFullPath(System.IO.Path.Combine(Application.dataPath, ".."));
+ string smmDir = System.IO.Path.Combine(gameRoot, "SMM");
+ string binDir = System.IO.Path.Combine(smmDir, "bin");
+ var ini = System.IO.Path.Combine(binDir, "mod_manager.ini");
+ if (!System.IO.File.Exists(ini)) return fallback;
+ foreach (var raw in System.IO.File.ReadAllLines(ini))
+ {
+ if (string.IsNullOrEmpty(raw)) continue;
+ var line = raw.Trim();
+ if (line.StartsWith("#") || line.StartsWith(";") || line.StartsWith("[")) continue;
+ var idx = line.IndexOf('='); if (idx <= 0) continue;
+ var k = line.Substring(0, idx).Trim();
+ var v = line.Substring(idx + 1).Trim();
+ if (k.Equals(key, StringComparison.OrdinalIgnoreCase)) return v;
+ }
+ }
+ catch { }
+ return fallback;
+ }
+
+ public static int ReadManagerInt(string key, int fallback)
+ {
+ string s = ReadManagerString(key, null);
+ if (s != null && int.TryParse(s, out int val)) return val;
+ return fallback;
+ }
+
+ private static Assembly ResolveShelteredApiAssembly()
+ {
+ // Prefer type-based resolve first so we bind to the already-loaded instance.
+ // This avoids duplicate loads and preserves compatibility for mods referencing both APIs.
+ try
+ {
+ var shelteredType = Type.GetType("ShelteredAPI.Core.ShelteredModManagerBase, ShelteredAPI", false);
+ if (shelteredType != null)
+ return shelteredType.Assembly;
+ }
+ catch { }
+
+ try
+ {
+ return Assembly.Load("ShelteredAPI");
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ private static void StartRetryRunner()
+ {
+ if (_runnerGo != null) return;
+ _runnerGo = new GameObject("HarmonyRetryRunner");
+ UnityEngine.Object.DontDestroyOnLoad(_runnerGo);
+ _runnerGo.AddComponent();
+ }
+
+ private static void TryInitializeShelteredApiCore(Assembly shelteredAssembly)
+ {
+ if (shelteredAssembly == null) return;
+
+ try
+ {
+ var bootstrapType = shelteredAssembly.GetType("ShelteredAPI.Core.ShelteredApiRuntimeBootstrap", false);
+ if (bootstrapType == null)
+ {
+ MMLog.WriteInfo("HarmonyBootstrap: ShelteredAPI runtime bootstrap type not found.");
+ return;
+ }
+
+ var init = bootstrapType.GetMethod("Initialize", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
+ if (init == null)
+ {
+ MMLog.WriteInfo("HarmonyBootstrap: ShelteredAPI runtime bootstrap Initialize method not found.");
+ return;
+ }
+
+ init.Invoke(null, null);
+ MMLog.WriteInfo("HarmonyBootstrap: ShelteredAPI runtime bootstrap initialized.");
+ }
+ catch (Exception ex)
+ {
+ MMLog.WriteWarning("HarmonyBootstrap: ShelteredAPI runtime bootstrap failed: " + ex.Message);
+ }
+ }
+
+ private static void LogPatchStatus(string label, string typeName, string methodName, string parameterTypeName = null)
+ {
+ try
+ {
+ Type targetType = AccessTools.TypeByName(typeName);
+ if (targetType == null)
+ {
+ MMLog.WriteInfo("HarmonyBootstrap: patch verify target type missing: " + label);
+ return;
+ }
+
+ MethodBase target = null;
+ if (string.IsNullOrEmpty(parameterTypeName))
+ {
+ target = AccessTools.Method(targetType, methodName);
+ }
+ else
+ {
+ Type parameterType = AccessTools.TypeByName(parameterTypeName);
+ if (parameterType == null)
+ {
+ MMLog.WriteInfo("HarmonyBootstrap: patch verify parameter type missing for " + label + ": " + parameterTypeName);
+ return;
+ }
+ target = AccessTools.Method(targetType, methodName, new[] { parameterType });
+ }
+
+ if (target == null)
+ {
+ MMLog.WriteInfo("HarmonyBootstrap: patch verify target method missing: " + label);
+ return;
+ }
+
+ var info = HarmonyLib.Harmony.GetPatchInfo(target);
+ bool patched = info != null && (
+ (info.Prefixes != null && info.Prefixes.Count > 0) ||
+ (info.Postfixes != null && info.Postfixes.Count > 0) ||
+ (info.Transpilers != null && info.Transpilers.Count > 0));
+
+ MMLog.WriteInfo("HarmonyBootstrap: patch verify " + label + " => " + (patched ? "patched" : "not patched"));
+ }
+ catch (Exception ex)
+ {
+ MMLog.WriteWarning("HarmonyBootstrap: patch verify failed for " + label + ": " + ex.Message);
+ }
+ }
+ }
+
+ internal class HarmonyRetryRunner : MonoBehaviour
+ {
+ private float _timer;
+ private int _attempts;
+ private const int MaxAttempts = 60;
+
+ private void Update()
+ {
+ _timer += Time.unscaledDeltaTime;
+ if (_timer < 0.5f) return;
+ _timer = 0f;
+ _attempts++;
+ MMLog.WriteDebug($"attempt {_attempts}");
+ HarmonyBootstrap.EnsurePatched();
+ if (_attempts >= MaxAttempts)
+ {
+ MMLog.Write("giving up waiting for 0Harmony");
+ Destroy(this.gameObject);
+ }
+ }
+ }
+}
diff --git a/Source/Spine/harmony/HarmonyCompat.cs b/Source/Spine/harmony/HarmonyCompat.cs
new file mode 100644
index 0000000..8af4e83
--- /dev/null
+++ b/Source/Spine/harmony/HarmonyCompat.cs
@@ -0,0 +1,212 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using UnityEngine;
+using Verse;
+
+namespace ModAPI.Core
+{
+ ///
+ /// RimWorld-backed logging shim for the imported Harmony utilities.
+ /// Preserves the old MMLog surface so the port stays localized.
+ ///
+ public static class MMLog
+ {
+ private const string Prefix = "[Spine.Harmony] ";
+ private static readonly HashSet WarnedKeys = new HashSet(StringComparer.Ordinal);
+
+ public static void Write(string message)
+ {
+ Log.Message(Prefix + (message ?? string.Empty));
+ }
+
+ public static void WriteInfo(string message)
+ {
+ Log.Message(Prefix + (message ?? string.Empty));
+ }
+
+ public static void WriteWarning(string message)
+ {
+ Log.Warning(Prefix + (message ?? string.Empty));
+ }
+
+ public static void WriteError(string message)
+ {
+ Log.Error(Prefix + (message ?? string.Empty));
+ }
+
+ public static void WriteDebug(string message)
+ {
+ if (!ShouldLogDebug())
+ {
+ return;
+ }
+
+ Log.Message(Prefix + "[Debug] " + (message ?? string.Empty));
+ }
+
+ public static void WarnOnce(string key, string message)
+ {
+ if (string.IsNullOrEmpty(key))
+ {
+ WriteWarning(message);
+ return;
+ }
+
+ lock (WarnedKeys)
+ {
+ if (!WarnedKeys.Add(key))
+ {
+ return;
+ }
+ }
+
+ Log.Warning(Prefix + (message ?? string.Empty));
+ }
+
+ private static bool ShouldLogDebug()
+ {
+ if (Prefs.DevMode)
+ {
+ return true;
+ }
+
+ try
+ {
+ return Better_Work_Tab.BetterWorkTabMod.Settings?.enableDebugLogging ?? false;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ }
+
+ ///
+ /// Compatibility preference surface for the imported transpiler framework.
+ /// Defaults are conservative and can later be wired to real settings.
+ ///
+ public static class ModPrefs
+ {
+ public static bool DebugTranspilers => Prefs.DevMode || TryGetBwtDebug();
+ public static bool TranspilerSafeMode => true;
+ public static bool TranspilerForcePreserveInstructionCount => false;
+ public static bool TranspilerFailFastCritical => false;
+ public static bool TranspilerCooperativeStrictBuild => false;
+ public static bool TranspilerQuarantineOnFailure => false;
+ public static bool TranspilerLogValidationWarnings => false;
+ public static bool TranspilerWarnOnVirtualCallMismatch => false;
+ public static bool TranspilerWarnOnExceptionHandlerMethods => false;
+
+ private static bool TryGetBwtDebug()
+ {
+ return TryGetBwtSetting(s => s.enableDebugLogging, false);
+ }
+
+ private static T TryGetBwtSetting(Func selector, T fallback)
+ {
+ if (selector == null)
+ {
+ return fallback;
+ }
+
+ try
+ {
+ var settings = Better_Work_Tab.BetterWorkTabMod.Settings;
+ return settings != null ? selector(settings) : fallback;
+ }
+ catch
+ {
+ return fallback;
+ }
+ }
+ }
+}
+
+namespace ModAPI.Reflection
+{
+ ///
+ /// Minimal reflection helper shim used by HarmonyHelper.SafeInvoke.
+ ///
+ public static class Safe
+ {
+ public static bool TryCall(object instance, string methodName, out T result, params object[] args)
+ {
+ result = default(T);
+
+ if (instance == null || string.IsNullOrEmpty(methodName))
+ {
+ return false;
+ }
+
+ try
+ {
+ var flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
+ var method = instance.GetType().GetMethod(methodName, flags);
+ if (method == null)
+ {
+ return false;
+ }
+
+ object value = method.Invoke(instance, args);
+ if (value is T typed)
+ {
+ result = typed;
+ return true;
+ }
+
+ if (value == null)
+ {
+ result = default(T);
+ return true;
+ }
+
+ result = (T)value;
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ }
+}
+
+namespace ModAPI.Spine
+{
+ public interface IPluginSettings
+ {
+ bool GetBool(string key, bool fallback);
+ }
+
+ public interface IPluginLog
+ {
+ void Info(string message);
+ }
+
+ public interface IPluginContext
+ {
+ IPluginSettings Settings { get; }
+ IPluginLog Log { get; }
+ }
+
+ ///
+ /// Placeholder save contract for imported helpers. RimWorld does not use the Sheltered save manager.
+ ///
+ public interface ISaveable
+ {
+ }
+
+ ///
+ /// Minimal compatibility stub so imported generic helpers still compile.
+ ///
+ public sealed class SaveManager : MonoBehaviour
+ {
+ public static SaveManager instance => null;
+
+ public bool HasBeenLoaded(ISaveable saveable)
+ {
+ return true;
+ }
+ }
+}
diff --git a/Source/Spine/harmony/HarmonyHelper.cs b/Source/Spine/harmony/HarmonyHelper.cs
new file mode 100644
index 0000000..4598c65
--- /dev/null
+++ b/Source/Spine/harmony/HarmonyHelper.cs
@@ -0,0 +1,216 @@
+using System;
+using System.Reflection;
+using HarmonyLib;
+using ModAPI.Core;
+using ModAPI.Reflection;
+using System.Linq;
+using System.Collections.Generic;
+
+namespace ModAPI.Harmony
+{
+ ///
+ /// Provides safe, high-level utilities for Harmony patching and reflection.
+ /// Reduces boilerplate and ensures mods don't crash the game due to missing methods.
+ ///
+ public static class HarmonyHelper
+ {
+ private static readonly HarmonyLib.Harmony _harmony = new HarmonyLib.Harmony("com.modapi.shared");
+
+ ///
+ /// Adds a Prefix patch to a method.
+ ///
+ /// The target class containing the method to patch.
+ /// The name of the method to patch.
+ /// The class containing the patch method.
+ /// The name of the prefix method.
+ public static void AddPrefix(string methodName, Type patchType, string patchMethodName)
+ {
+ var prefix = new HarmonyMethod(AccessTools.Method(patchType, patchMethodName));
+ TryPatchMethod(typeof(T), methodName, prefix: prefix);
+ }
+
+ ///
+ /// Adds a Postfix patch to a method.
+ ///
+ /// The target class containing the method to patch.
+ /// The name of the method to patch.
+ /// The class containing the patch method.
+ /// The name of the postfix method.
+ public static void AddPostfix(string methodName, Type patchType, string patchMethodName)
+ {
+ var postfix = new HarmonyMethod(AccessTools.Method(patchType, patchMethodName));
+ TryPatchMethod(typeof(T), methodName, postfix: postfix);
+ }
+
+ ///
+ /// Attempts to patch a method with provided Harmony methods.
+ /// Logs errors if the target method is not found.
+ ///
+ public static bool TryPatchMethod(Type targetType, string methodName,
+ HarmonyMethod prefix = null, HarmonyMethod postfix = null,
+ HarmonyMethod transpiler = null)
+ {
+ try
+ {
+ if (targetType == null)
+ {
+ MMLog.WriteError("[HarmonyHelper] Target type is null.");
+ return false;
+ }
+
+ var original = AccessTools.Method(targetType, methodName);
+ if (original == null)
+ {
+ MMLog.WriteError($"[HarmonyHelper] Target method '{methodName}' not found on type '{targetType.FullName}'.");
+ return false;
+ }
+
+ _harmony.Patch(original, prefix, postfix, transpiler);
+
+ string patchType = prefix != null ? "Prefix" : (postfix != null ? "Postfix" : "Transpiler");
+ MMLog.Write($"[HarmonyHelper] Success: Applied {patchType} to {targetType.Name}.{methodName}");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ MMLog.WriteError($"[HarmonyHelper] Critical error patching {targetType?.Name}.{methodName}: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// Safely invokes a method via reflection, returning the result or default(T) on failure.
+ /// Wraps ModAPI.Reflection.Safe.TryCall.
+ ///
+ public static T SafeInvoke(object instance, string methodName, params object[] args)
+ {
+ T result;
+ if (Safe.TryCall(instance, methodName, out result, args))
+ {
+ return result;
+ }
+ return default(T);
+ }
+
+ ///
+ /// Patch ALL overloads of a method by name.
+ /// Useful for methods like Foo(), Foo(int), Foo(string), etc.
+ ///
+ /// The Harmony instance to use for patching.
+ /// Target type containing the method.
+ /// Name of the method to patch.
+ /// Optional filter: ONLY patch overloads matching these param types. Use null for no filter (all overloads).
+ /// If true, skip generic methods. Default: false.
+ /// Prefix patch.
+ /// Postfix patch.
+ /// Transpiler patch.
+ ///
+ /// Only patches methods declared on the specified type.
+ /// Does NOT patch inherited methods or base class overloads.
+ ///
+ public static void PatchAllOverloads(
+ HarmonyLib.Harmony harmony,
+ Type type,
+ string methodName,
+ Type[] parameterTypes = null,
+ bool ignoreGenerics = false,
+ HarmonyMethod prefix = null,
+ HarmonyMethod postfix = null,
+ HarmonyMethod transpiler = null)
+ {
+ if (harmony == null || type == null || string.IsNullOrEmpty(methodName))
+ throw new ArgumentNullException();
+
+ if (prefix == null && postfix == null && transpiler == null)
+ {
+ MMLog.WriteWarning("[HarmonyHelper.PatchAllOverloads] No patches provided");
+ return;
+ }
+
+ var methods = type.GetMethods(
+ BindingFlags.Public | BindingFlags.NonPublic |
+ BindingFlags.Instance | BindingFlags.Static);
+
+ var matches = methods.Where(m =>
+ m.Name == methodName &&
+ (!ignoreGenerics || !m.IsGenericMethodDefinition))
+ .ToList();
+
+ // Optional parameter type filtering
+ if (parameterTypes != null)
+ {
+ matches = matches.Where(m =>
+ {
+ var mparams = m.GetParameters().Select(p => p.ParameterType).ToArray();
+ if (mparams.Length != parameterTypes.Length) return false;
+
+ for (int i = 0; i < parameterTypes.Length; i++)
+ {
+ if (parameterTypes[i] != mparams[i]) return false;
+ }
+ return true;
+ }).ToList();
+ }
+
+ if (matches.Count == 0)
+ {
+ MMLog.WriteWarning(
+ $"[HarmonyHelper.PatchAllOverloads] No overloads found for " +
+ $"{type.FullName}.{methodName}");
+ return;
+ }
+
+ int successCount = 0;
+ var failedSignatures = new List();
+
+ foreach (var method in matches)
+ {
+ var sig = method.GetParameters().Length == 0
+ ? "()"
+ : $"({string.Join(", ", method.GetParameters().Select(p => p.ParameterType.Name).ToArray())})";
+
+ try
+ {
+ harmony.Patch(method, prefix, postfix, transpiler);
+ successCount++;
+
+ MMLog.WriteDebug(
+ $"[HarmonyHelper] Patched overload {type.Name}.{methodName}{sig}");
+ }
+ catch (Exception ex)
+ {
+ failedSignatures.Add($"{methodName}{sig}");
+ MMLog.WriteError(
+ $"[HarmonyHelper] Failed to patch {type.Name}.{methodName}{sig}: " +
+ $"{ex.Message}");
+ }
+ }
+
+ if (failedSignatures.Count > 0)
+ {
+ MMLog.WriteWarning(
+ $"[HarmonyHelper] {failedSignatures.Count} overload(s) failed: " +
+ string.Join(", ", failedSignatures.ToArray()));
+ }
+
+ MMLog.Write(
+ $"[HarmonyHelper] PatchAllOverloads completed: {successCount}/{matches.Count} " +
+ $"overloads patched for {type.Name}.{methodName}");
+ }
+
+ ///
+ /// Patch all parameterless overloads (no arguments).
+ /// Convenience wrapper for PatchAllOverloads with Type.EmptyTypes.
+ ///
+ public static void PatchAllParameterlessOverloads(
+ HarmonyLib.Harmony harmony,
+ Type type,
+ string methodName,
+ HarmonyMethod prefix = null,
+ HarmonyMethod postfix = null,
+ HarmonyMethod transpiler = null)
+ {
+ PatchAllOverloads(harmony, type, methodName, Type.EmptyTypes, false, prefix, postfix, transpiler);
+ }
+ }
+}
diff --git a/Source/Spine/harmony/HarmonyUtil.cs b/Source/Spine/harmony/HarmonyUtil.cs
new file mode 100644
index 0000000..b2f3014
--- /dev/null
+++ b/Source/Spine/harmony/HarmonyUtil.cs
@@ -0,0 +1,389 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using HarmonyLib;
+using UnityEngine;
+using ModAPI.Core;
+using ModAPI.Spine;
+
+namespace ModAPI.Harmony
+{
+ public static class HarmonyUtil
+ {
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
+ public sealed class DebugPatchAttribute : Attribute
+ {
+ public string Key;
+ public DebugPatchAttribute() { }
+ public DebugPatchAttribute(string key) { Key = key; }
+ }
+
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
+ public sealed class DangerousAttribute : Attribute
+ {
+ public string Reason;
+ public DangerousAttribute() { }
+ public DangerousAttribute(string reason) { Reason = reason; }
+ }
+
+ public sealed class PatchOptions
+ {
+ public bool AllowDebugPatches;
+ public bool AllowDangerousPatches;
+ public bool AllowStructReturns;
+ public string[] Before;
+ public string[] After;
+ public int? Priority;
+ public Action