From 8295a0208b344e461827b1f83cc8695a42e089b7 Mon Sep 17 00:00:00 2001 From: Jaxe Date: Mon, 20 Aug 2018 14:04:47 +0800 Subject: [PATCH] Initial commit --- About/About.xml | 9 + About/ModSync.xml | 15 + About/Preview.png | Bin 0 -> 2088 bytes Assemblies/PawnRules.xml | 243 ++++++++++++++ Defs/WorldObjectDefs/PawnRules.xml | 11 + Languages/English/Keyed/PawnRules.xml | 121 +++++++ README.md | 56 ++++ Source/Controller.cs | 19 ++ Source/Data/AddonManager.cs | 33 ++ Source/Data/AddonOption.cs | 49 +++ Source/Data/Binding.cs | 59 ++++ Source/Data/IPresetableType.cs | 9 + Source/Data/Lang.cs | 9 + Source/Data/PawnType.cs | 40 +++ Source/Data/Presetable.cs | 54 ++++ Source/Data/Registry.cs | 297 ++++++++++++++++++ Source/Data/Restriction.cs | 41 +++ Source/Data/RestrictionTemplate.cs | 104 ++++++ Source/Data/RestrictionType.cs | 31 ++ Source/Data/Rules.cs | 157 +++++++++ Source/Data/ScribePlus.cs | 30 ++ Source/Data/Toggle.cs | 17 + Source/Interface/Dialog_Alert.cs | 78 +++++ Source/Interface/Dialog_Global.cs | 20 ++ Source/Interface/Dialog_PresetName.cs | 91 ++++++ Source/Interface/Dialog_Restrictions.cs | 172 ++++++++++ Source/Interface/Dialog_Rules.cs | 257 +++++++++++++++ Source/Interface/GuiPlus.cs | 182 +++++++++++ Source/Interface/Listing_Preset.cs | 146 +++++++++ Source/Interface/Listing_StandardPlus.cs | 112 +++++++ Source/Interface/OptionWidget.cs | 9 + Source/Interface/WindowPlus.cs | 60 ++++ Source/Mod.cs | 24 ++ Source/Patch/Extensions.cs | 145 +++++++++ Source/Patch/PrivateAccess.cs | 25 ++ ...imWorld_FoodUtility_BestFoodInInventory.cs | 36 +++ ...imWorld_FoodUtility_BestFoodSourceOnMap.cs | 137 ++++++++ .../RimWorld_GenConstruct_CanConstruct.cs | 26 ++ ...tionWorker_RomanceAttempt_SuccessChance.cs | 24 ++ Source/Patch/RimWorld_JobGiver_PackFood.cs | 21 ++ Source/Patch/RimWorld_JoyGiver_Ingest.cs | 21 ++ ...d_PawnUtility_TrySpawnHatchedOrBornPawn.cs | 17 + ...RelationsUtility_TryDevelopBondRelation.cs | 20 ++ Source/Patch/Verse_Game_FinalizeInit.cs | 11 + Source/Patch/Verse_Pawn_GetGizmos.cs | 18 ++ .../Verse_Pawn_GuestTracker_SetGuestStatus.cs | 17 + Source/Patch/Verse_Pawn_Kill.cs | 16 + Source/Patch/Verse_Pawn_SetFaction.cs | 17 + Source/Patch/Verse_Pawn_SetFactionDirect.cs | 17 + Source/PawnRules.csproj | 131 ++++++++ Source/PawnRules.sln | 22 ++ Source/Properties/AssemblyInfo.cs | 9 + Source/SDK/OptionHandle.cs | 144 +++++++++ Source/SDK/OptionTarget.cs | 28 ++ Source/SDK/PawnRulesLink.cs | 127 ++++++++ Textures/UI/EditRules.png | Bin 0 -> 2088 bytes 56 files changed, 3584 insertions(+) create mode 100644 About/About.xml create mode 100644 About/ModSync.xml create mode 100644 About/Preview.png create mode 100644 Assemblies/PawnRules.xml create mode 100644 Defs/WorldObjectDefs/PawnRules.xml create mode 100644 Languages/English/Keyed/PawnRules.xml create mode 100644 README.md create mode 100644 Source/Controller.cs create mode 100644 Source/Data/AddonManager.cs create mode 100644 Source/Data/AddonOption.cs create mode 100644 Source/Data/Binding.cs create mode 100644 Source/Data/IPresetableType.cs create mode 100644 Source/Data/Lang.cs create mode 100644 Source/Data/PawnType.cs create mode 100644 Source/Data/Presetable.cs create mode 100644 Source/Data/Registry.cs create mode 100644 Source/Data/Restriction.cs create mode 100644 Source/Data/RestrictionTemplate.cs create mode 100644 Source/Data/RestrictionType.cs create mode 100644 Source/Data/Rules.cs create mode 100644 Source/Data/ScribePlus.cs create mode 100644 Source/Data/Toggle.cs create mode 100644 Source/Interface/Dialog_Alert.cs create mode 100644 Source/Interface/Dialog_Global.cs create mode 100644 Source/Interface/Dialog_PresetName.cs create mode 100644 Source/Interface/Dialog_Restrictions.cs create mode 100644 Source/Interface/Dialog_Rules.cs create mode 100644 Source/Interface/GuiPlus.cs create mode 100644 Source/Interface/Listing_Preset.cs create mode 100644 Source/Interface/Listing_StandardPlus.cs create mode 100644 Source/Interface/OptionWidget.cs create mode 100644 Source/Interface/WindowPlus.cs create mode 100644 Source/Mod.cs create mode 100644 Source/Patch/Extensions.cs create mode 100644 Source/Patch/PrivateAccess.cs create mode 100644 Source/Patch/RimWorld_FoodUtility_BestFoodInInventory.cs create mode 100644 Source/Patch/RimWorld_FoodUtility_BestFoodSourceOnMap.cs create mode 100644 Source/Patch/RimWorld_GenConstruct_CanConstruct.cs create mode 100644 Source/Patch/RimWorld_InteractionWorker_RomanceAttempt_SuccessChance.cs create mode 100644 Source/Patch/RimWorld_JobGiver_PackFood.cs create mode 100644 Source/Patch/RimWorld_JoyGiver_Ingest.cs create mode 100644 Source/Patch/RimWorld_PawnUtility_TrySpawnHatchedOrBornPawn.cs create mode 100644 Source/Patch/RimWorld_RelationsUtility_TryDevelopBondRelation.cs create mode 100644 Source/Patch/Verse_Game_FinalizeInit.cs create mode 100644 Source/Patch/Verse_Pawn_GetGizmos.cs create mode 100644 Source/Patch/Verse_Pawn_GuestTracker_SetGuestStatus.cs create mode 100644 Source/Patch/Verse_Pawn_Kill.cs create mode 100644 Source/Patch/Verse_Pawn_SetFaction.cs create mode 100644 Source/Patch/Verse_Pawn_SetFactionDirect.cs create mode 100644 Source/PawnRules.csproj create mode 100644 Source/PawnRules.sln create mode 100644 Source/Properties/AssemblyInfo.cs create mode 100644 Source/SDK/OptionHandle.cs create mode 100644 Source/SDK/OptionTarget.cs create mode 100644 Source/SDK/PawnRulesLink.cs create mode 100644 Textures/UI/EditRules.png diff --git a/About/About.xml b/About/About.xml new file mode 100644 index 0000000..13054a3 --- /dev/null +++ b/About/About.xml @@ -0,0 +1,9 @@ + + + + Pawn Rules + Jaxe + 0.19.0 + Pawn Rules is a mod that allows custom rules to be assigned individually to your colonists, animals, guests and prisoners.\n\nCurrently the following rules can be applied:\n\n- Disallow certain foods\n- Disallow bonding with certain animals\n- Disallow new romances\n- Disallow constructing items that have a quality level\n\n + https://ludeon.com/forums/index.php?topic=43086.0 + diff --git a/About/ModSync.xml b/About/ModSync.xml new file mode 100644 index 0000000..80351af --- /dev/null +++ b/About/ModSync.xml @@ -0,0 +1,15 @@ + + + + 59f538ed-f86d-4506-a4a5-7e9faaa37509 + Pawn Rules + v1.0-rw0.19 + False + + Jaxe-Dev + PawnRules + About + ModSyncReleases + master + + diff --git a/About/Preview.png b/About/Preview.png new file mode 100644 index 0000000000000000000000000000000000000000..143a9b8ab12c2a0ec0a327b3e7600bbbe89d58f3 GIT binary patch literal 2088 zcmV+@2-o+CP)@8efxA|eA6mG6CO8v zYonl`An7o8yg7g?MBnEF0|QxCSC>Tz?B?diu3x|YO0ZWl*%}RmNchgqPRl}g_7lT+ z#v(yi3qiN-W`n|5M?0p2l)x+G%lpMitwjVo>1g+u)L3AOg#QRdt*nWJ4n=z)${ki6 z3&@4vy?Zx%@ZbR}D=TBbF4XiH_!Em)0EEl{*Ma-NT5y!u9wG_9ckf=7k&(eXJUm!v zXec{$=#as6-fS*_&N!j)QYh;nxEa1XD!Xr?IFb@Y5TpGJ%AM4(XU`sX?AS4#8;y*N zj2#+{mt|16hTvjNO^ptegq9^3=>mQiTq4%h;!N@Raa0Ei73%Bjg}%N%;rQ|60)!ts z7s4Bax{DVtmd@EqrI(kN&N&qo71I9|a+yuBfFiK}!i5VN9-?UY2L=XMOG^uNWG95Y zfu;b-yhB1lm;}p*hlkmbBS%ohrigfR4NilLtPfuq{mo8-q3A%yY!-o&q`t|F%5;3@0Bg)`mj0+gM3f!Wf+qZA4 zHsd2sXHTC#m8#`r6y4q3EIK-xAtX0=&66ij_{iVAdzY%E;H{WQdnY3)%1!j>(WBf< z0|EjR3E$t}&rY8{&4z}CsQMd(pj9Ce^MM0=`t!)hNOtw=RW^VA{IP3#dU{xRcsOfr zZl-FPDEu=;Sra2yfgF`r0OBgJo2(ym!CLwcmLQCdj`DtBsS_Z)bj=m8v%LSNxPU3K zz}Fa|^CIAQPoW3&ix~cb-ZKX9jO{qQQc_alL*aN_TpXV;vEJPHC7rb%;fa4<#;iejxI7=nEF1($c9m~nd;lU8C z7Qw+h(-oozqb*MX5YP);Onyx(E@6ArGsXYT$c0Z%PUhDTD&4hf*Q5tS`UAx9enE*RG#2lZ5^2TW6#*KW7%imwu<7#VbS!`@9N0gvkxY9Ob zbArZBf_#FBiHT8d(xVLvF`dOEY;o$;DQ+2Z(igt2u8#Ku)f2^X5&4$WE%#R`&N4|0gCUvdYRzgV&HXY}l}Y#|5B^5I$K+4&4|kp`YyRY`*@g z5e(($=S%6VYzQ(;%N)$u9s$xe@@?APktbd#_K%8+V$YuO_~&O_^M#^Ln6L_Dnjska z1#F|&my7hDpP!!qMe%<0_Vx-sK0eYldHR9SWCcSSjL?3Sf}uVU2N;^U3D-L#_z8D% za$-;f&qH9cr2n)M6jn;621C7K3hJ^M7#(t@E@F(pp@ zB+zjSZ6j(v!y>lE#L4kbbKHBzzb0pr1&BMdYot8B5;QEv_yrcD00^H=a9zbde2aTh zax(#n=$h^dpzY^0diz47(Wnv(5vT(8|CDBXaQt*v0MEZ2Ja~{DK73e}4#daDGhbg{ z_Wb$tI9S44u!5nN43DU{Wx2?%X-{^5siaI#5(p zBoBrjO?Mag3H*&#tMy*CY#Ad~8VpfBKzv{MjQl-;>AVa^(u9RkTPGVirIbZ6hD9uc z=qT;Ve1Zv!vKYQ9Bt;w5hPRUfh0*CpQ{l5ytpIytb8gnAfOckOG$94hi^mgv_jbAv z_1P2!@byUlGaC6@*Op3RCW#-82ZSgq%d@W5M%C5TI$-YAkn1KCH&f!!)6uYGu1eIvXg46fhUX4^t7E znwfG9?H-Ny;Uw(?QtCUHS#IiVRP=euIvL!~vQhf+^#9)uTR7Rw^v~!25nuova1fH( S7iSOv0000 + + + PawnRules + + + + + The base class of a rules option handle. + + + + + Called when the button for this option is clicked. Setting the value must be handled by the delegate. Unused if this option does not implement a button widget. + + The pawn being displayed when the button was clicked. + Currently unused but as practice return true if the value was changed. + + + + Called when the button for this option is clicked in the default rules dialog. Setting the value must be handled by the delegate. Unused if this option does not implement a button widget. + + The target of the default rule. + Currently unused but as practice return true if the value was changed. + + + + Called when the button for this option is clicked. Unused if this option does not implement a button widget. + + + + + Called when the button for this option is clicked in the default rules dialog. Unused if this option does not implement a button widget. + + + + + Used to see if a given pawn has this option. + + The pawn to query. + + + + + Provides a handle for a rules option. This class may not be instantiated manually, create a to add one. + + The value type. + + + + This is called when the value of the option is changing for a pawn. Unused if the option implements a button. + + The pawn whose rule option is being changed. + The original value of the option. + The new value of the option entered in the rules dialog. + The return value will be the value set for the option. + + + + This is called when the value of the option is changing for a preset. Unused if the option implements a button. + + The target of the preset that is being changed. + The original value of the option. + The new value of the option entered in the rules dialog. + The return value will be the value set for the option. + + + + This is called when the value of the option is changing for a pawn. Unused if option option implements a button. + + + + + This is called when the value of the option is changing for a preset type. Unused if the option implements a button. + + + + + Gets or sets the displayed label of this option. + + + + + Gets or sets the tooltip of this option. + + + + + Gets the value of this option for the given pawn. + + The pawn to get the value from. + The value returned if unable to retrieve the option. + Returns the value if the option is found or if not. + + + + Gets the default value of this option for the given target. + + The default rules target to get the value from. + The value returned if unable to retrieve the option. + Returns the value if the option is found or if not. + + + + Sets the value of this option for the given pawn. + + The pawn to set the value to. + The new value for the option. + Returns true if the option was successfully set. + + + + Used to set the target type of the pawn that a rule will be applied to. Multiple flags may be set. + + + + + For all colonists part of the player's faction. + + + + + For all animals part of the player's faction. + + + + + For all guests that are currently staying with the player's faction. + + + + + For all prisoners that are being held by the player's faction. + + + + + Provides a link to Pawn Rules and is used to add options to the rules dialog. + + + + + Initializes a link to Pawn Rules. Only one plugin per mod is allowed. + + + + + Adds a new Toggle to the Rules dialog that sets a . + + The key used in the save file. Will be automatically prefixed with your Mod Identifier. + The type(s) of pawns this option will apply to. + The label of the widget displayed. + The tooltip displayed for the widget. + This is the default value if is false or no default rules exist when a pawn is first given rules. + If set to false it cannot be used in a preset. + Returns a handle for this option. + + + + Adds a new Entry to the Rules dialog that sets a value. + + The key used in the save file. Will be automatically prefixed with your Mod Identifier. + The type(s) of pawns this option will apply to. + The label of the widget displayed. + The tooltip displayed for the widget. + This is the default value if is false or no default rules exist when a pawn is first given rules. + If set to false it cannot be used in a preset. + Returns a handle for this option. + + + + Adds a new Entry to the Rules dialog that sets an value. + + The key used in the save file. Will be automatically prefixed with your Mod Identifier. + The type(s) of pawns this option will apply to. + The label of the widget displayed. + The tooltip displayed for the widget. + This is the default value if is false or no default rules exist when a pawn is first given rules. + If set to false it cannot be used in a preset. + Returns a handle for this option. + + + + Adds a new Entry to the Rules dialog that sets a value. + + The key used in the save file. Will be automatically prefixed with your Mod Identifier. + The type(s) of pawns this option will apply to. + The label of the widget displayed. + The tooltip displayed for the widget. + This is the default value if is false or no default rules exist when a pawn is first given rules. + If set to false it cannot be used in a preset. + Returns a handle for this option. + + + + Adds a new Button to the Rules dialog that sets a value. Buttons require to be used to set the value. + + The key used in the save file. Will be automatically prefixed with your Mod Identifier. + The type(s) of pawns this option will apply to. + The label of the widget displayed. + The tooltip displayed for the widget. + This is the default value if is false or no default rules exist when a pawn is first given rules. + If set to false it cannot be used in a preset. + Returns a handle for this option. + + + + Adds a new Button to the Rules dialog that sets a value. Buttons require to be used to set the value. + + The key used in the save file. Will be automatically prefixed with your Mod Identifier. + The type(s) of pawns this option will apply to. + The label of the widget displayed. + The tooltip displayed for the widget. + This is the default value if is false or no default rules exist when a pawn is first given rules. + If set to false it cannot be used in a preset. + Returns a handle for this option. + + + + Adds a new Button to the Rules dialog that sets an value. Buttons require to be used to set the value. + + The key used in the save file. Will be automatically prefixed with your Mod Identifier. + The type(s) of pawns this option will apply to. + The label of the widget displayed. + The tooltip displayed for the widget. + This is the default value if is false or no default rules exist when a pawn is first given rules. + If set to false it cannot be used in a preset. + Returns a handle for this option. + + + + Adds a new Button to the Rules dialog that sets a value. Buttons to be used to set the value. + + The key used in the save file. Will be automatically prefixed with your Mod Identifier. + The type(s) of pawns this option will apply to. + The label of the widget displayed. + The tooltip displayed for the widget. + This is the default value if is false or no default rules exist when a pawn is first given rules. + If set to false it cannot be used in a preset. + Returns a handle for this option. + + + diff --git a/Defs/WorldObjectDefs/PawnRules.xml b/Defs/WorldObjectDefs/PawnRules.xml new file mode 100644 index 0000000..e986129 --- /dev/null +++ b/Defs/WorldObjectDefs/PawnRules.xml @@ -0,0 +1,11 @@ + + + + + + PawnRules_Registry + + PawnRules.Data.Registry + + + diff --git a/Languages/English/Keyed/PawnRules.xml b/Languages/English/Keyed/PawnRules.xml new file mode 100644 index 0000000..3a89033 --- /dev/null +++ b/Languages/English/Keyed/PawnRules.xml @@ -0,0 +1,121 @@ + + + + OK + Cancel + Yes + No + + Assign to ... + Click to assign this preset to others. + + Showing rules for {0} + Showing default rules for {0} + Click to view rules for other types. + + Options + Click to open the global options dialog. + + Remove Mod + This will remove and all rules from all characters in the current game and deactivate the mod.\n\nThis will avoid any errors being displayed the first time reloading this game without this mod. + This will remove all rules from all characters in the current game and deactivate the mod.\n\nAre you sure you want to do this? + A save was created called '{0}' that contains no traces of this mod.\n\nThe game will now restart. + A save was created called '{0}' that contains no traces of this mod or anyone of the following addon mods:\n\n{1}\n\nThe game will now restart. + + New + Create a new preset. + Edit + Modify the currently selected preset. + Save + Commit changes to this preset. + Save As + Create a preset from the currently displayed rules. + Clear + Reset these personalized rules to vanilla. + Revert + Cancel changes to this preset. + Rename + Change the name of this preset. + Delete + Permanently remove this preset. + Are you sure you want to delete the preset [{0}]?\n\nThis will affect all characters using this preset. + Save changes to this preset? + + Allow all + Disallow all + + Rules Preset + Restriction Preset + + Presets + None + <i>Personalized</i> + + Food Preset + Food Presets + Edibles + Bonding Preset + Bonding Presets + Animals + + Colonist + Colonists + Animal + Animals + Guest + Guests + Prisoner + Prisoners + Person + Persons + Individual + + Food Restrictions: {0} + Click to choose foods that can be eaten automatically. + Bonding Restrictions: {0} + Click to choose animals that can be bonded with. + Allow Courting + If toggled off will not be allowed to gain a new lover or spouse. + Allow Artisan + If toggled off will not build any construction that has a quality level. + not allowed artisan builds + construct {0} + + Common Pets + Farm Animals + Herd Animals + Predators + Insects + Other + + Rules + Edit the rules for this {0}. + + Global Options + + Rules for {0}, {1} + Default Rules for {0} + Options + Configuration + Default {0} + Assign to all {0} + Are you sure you want to {0} {1} other {2}? + Set as default for new {0} + Are you sure you want to change the default rules for new {0} to the preset {1}? + Assign to {0} + Are you sure you want to {0} {1}? + assign these personalized rules to + assign the preset {0} to + clear the rules for + Edit restrictions... + Allow all {0} + Set to {0} + + {0} [{1}] + {0} for {1}, {2} + Category + + Preset [{0}] + Create Preset + Enter new name: + diff --git a/README.md b/README.md new file mode 100644 index 0000000..ce2473b --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# Pawn Rules +#### Version 1.0 +Built for **RimWorld 1.0.x / 0.19.x**\ +Supports **ModSync RW** + +[Link to Ludeon Forum Post](https://ludeon.com/forums/index.php?topic=43086.0) + +------------ + +Pawn Rules is a mod that allows custom rules to be assigned individually to your colonists, animals, guests and prisoners. + +Currently the following rules can be applied: +- Disallow certain foods + - *Ignored if binging on food* +- Disallow bonding with certain animals + - *This has no effect on existing bonds* +- Disallow new romances + - *This has no effect on existing relations and engaged couples can still get married* +- Disallow constructing items that have a quality level + - *Can still haul materials to blueprints* + +------------ + +Supports addons created by other modders by allowing easy creation of new rule options while handling the GUI and world storage saving. For more information check out the [wiki page on Addons](https://github.com/Jaxe-Dev/PawnRules/wiki/Addons). + +------------ + +##### INSTALLATION +- **[Download the latest release](https://github.com/Jaxe-Dev/PawnRules/releases/latest) and unzip it into your *RimWorld/Mods* folder.** + +------------ + +###### TECHINICAL DETAILS +>This mod can be safely removed from a save without breaking the game. To do so go to *Global Options* either from the mod settings menu or the main rules window for a character and select *Remove Mod*. +> +> A save of your world will be made with no traces of this mod and the game will restart. Skipping the *Remove Mod* step will result in errors being displayed the first time a save is loaded although no further problems should occur. + +------------ + +The following original methods are patched using HugsLib.Harmony: +```C# +RimWorld.FoodUtility.BestFoodSourceOnMap : Prefix +RimWorld.FoodUtility.BestFoodInInventory : Prefix +RimWorld.GenConstruct.CanConstruct : Postfix +RimWorld.InteractionWorker_RomanceAttempt.SuccessChance : Prefix +RimWorld.JobGiver_PackFood.IsGoodPackableFoodFor : Postfix +RimWorld.JoyGiver_Ingest.CanIngestForJoy : Prefix +RimWorld.PawnUtility.TrySpawnHatchedOrBornPawn : Postfix +RimWorld.RelationsUtility.TryDevelopBondRelation : Prefix +Verse.Game.FinalizeInit : Postfix +Verse.Pawn.GetGizmos : Postfix +Verse.Pawn.Kill : Postfix +Verse.Pawn.SetFaction : Prefix +Verse.Pawn.SetFactionDirect : Prefix +Verse.Pawn_GuestTracker.SetGuestStatus : Prefix +``` diff --git a/Source/Controller.cs b/Source/Controller.cs new file mode 100644 index 0000000..a740e47 --- /dev/null +++ b/Source/Controller.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using Harmony; +using PawnRules.Data; +using Verse; + +namespace PawnRules +{ + [StaticConstructorOnStartup] + internal static class Controller + { + static Controller() => HarmonyInstance.Create(Mod.Id).PatchAll(Assembly.GetExecutingAssembly()); + + public static void LoadWorld() + { + AddonManager.AcceptingAddons = false; + Registry.Initialize(); + } + } +} diff --git a/Source/Data/AddonManager.cs b/Source/Data/AddonManager.cs new file mode 100644 index 0000000..717d0aa --- /dev/null +++ b/Source/Data/AddonManager.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; +using Verse; + +namespace PawnRules.Data +{ + internal static class AddonManager + { + private static readonly Regex KeyRegex = new Regex("[a-zA-Z0-9_]+"); + + private static readonly Dictionary OptionRegistry = new Dictionary(); + private static readonly List ModRegistry = new List(); + + public static bool AcceptingAddons { get; set; } = true; + public static bool HasAddons { get; private set; } + + public static IEnumerable Options => OptionRegistry.Values; + public static IEnumerable Mods => ModRegistry; + + public static void Add(AddonOption addon) + { + if (Registry.IsActive || !AcceptingAddons) { throw new Mod.Exception("Options must be added before a world is loaded"); } + if (string.IsNullOrEmpty(addon.Key)) { throw new Mod.Exception("Key is null or empty"); } + if (!KeyRegex.IsMatch(addon.Key)) { throw new Mod.Exception("Key must not contain any non-alphanumeric characters except for an underscore"); } + if (OptionRegistry.ContainsKey(addon.Key)) { throw new Mod.Exception("Key already exists"); } + + if (!ModRegistry.Contains(addon.Link.ModContentPack)) { ModRegistry.Add(addon.Link.ModContentPack); } + OptionRegistry.Add(addon.Key, addon); + + HasAddons = true; + } + } +} diff --git a/Source/Data/AddonOption.cs b/Source/Data/AddonOption.cs new file mode 100644 index 0000000..90eb55d --- /dev/null +++ b/Source/Data/AddonOption.cs @@ -0,0 +1,49 @@ +using System; +using PawnRules.Interface; +using PawnRules.SDK; +using Verse; + +namespace PawnRules.Data +{ + internal class AddonOption + { + public string Key { get; } + public OptionTarget Target { get; } + public OptionWidget Widget { get; } + public string Label { get; set; } + public string Tooltip { get; set; } + public Type Type { get; } + + public object DefaultValue { get; } + public bool AllowedInPreset { get; } + + public OptionHandle Handle { get; private set; } + public PawnRulesLink Link { get; } + public float WidgetHeight => (Widget == OptionWidget.Button ? 30f : Text.LineHeight) + 2f; + + private AddonOption(PawnRulesLink link, string key, OptionTarget target, OptionWidget widget, string label, string tooltip, Type type, object defaultValue, bool allowedInPreset) + { + Link = link; + Key = key; + Target = target; + Widget = widget; + Label = label; + Tooltip = tooltip; + Type = type; + DefaultValue = defaultValue; + AllowedInPreset = allowedInPreset; + } + + internal static OptionHandle Add(PawnRulesLink link, string key, OptionTarget target, OptionWidget widget, string label, string tooltip, T defaultValue, bool allowedInPreset = true) + { + var option = new AddonOption(link, link.ModContentPack.Identifier + "_" + key, target, widget, label, tooltip, typeof(T), defaultValue, allowedInPreset); + + AddonManager.Add(option); + + var handle = new OptionHandle(option); + option.Handle = handle; + + return handle; + } + } +} diff --git a/Source/Data/Binding.cs b/Source/Data/Binding.cs new file mode 100644 index 0000000..f63229c --- /dev/null +++ b/Source/Data/Binding.cs @@ -0,0 +1,59 @@ +using PawnRules.Patch; +using Verse; + +namespace PawnRules.Data +{ + internal class Binding : IExposable + { + public Pawn Pawn; + public PawnType Target; + public Rules Rules; + + private Rules _individual; + private Rules _preset; + + public Binding() + { } + + public Binding(Pawn pawn, Rules rules) : this() + { + Pawn = pawn; + Rules = rules; + } + + public Binding(PawnType target, Rules rules) : this() + { + Target = target; + Rules = rules; + } + + public void ExposeData() + { + if ((Scribe.mode != LoadSaveMode.Saving) || (Target == null)) { Scribe_References.Look(ref Pawn, "pawn"); } + if ((Scribe.mode != LoadSaveMode.Saving) || (Target != null)) { Target = PawnType.FromId(ScribePlus.LookValue(Target?.Id, "target")); } + + + if (Scribe.mode != LoadSaveMode.Saving) + { + Scribe_References.Look(ref _preset, "preset"); + if (Target == null) + { + Scribe_Deep.Look(ref _individual, "rules"); + _individual?.SetPawn(Pawn); + } + } + else if (Rules != null) + { + if (Rules.IsPreset) { Scribe_References.Look(ref Rules, "preset"); } + else { Scribe_Deep.Look(ref Rules, "rules"); } + } + + if (Scribe.mode != LoadSaveMode.PostLoadInit) { return; } + + if (_preset != null) { Rules = _preset; } + else if (_individual != null) { Rules = _individual; } + else if (Pawn != null) { Rules = Registry.GetVoidPreset(Pawn.GetTargetType()).CloneRulesFor(Pawn); } + else { throw new Mod.Exception("Unable to load rules for binding"); } + } + } +} diff --git a/Source/Data/IPresetableType.cs b/Source/Data/IPresetableType.cs new file mode 100644 index 0000000..4aa9ce9 --- /dev/null +++ b/Source/Data/IPresetableType.cs @@ -0,0 +1,9 @@ +namespace PawnRules.Data +{ + internal interface IPresetableType + { + string Id { get; } + string Label { get; } + string LabelPlural { get; } + } +} diff --git a/Source/Data/Lang.cs b/Source/Data/Lang.cs new file mode 100644 index 0000000..3b8c25f --- /dev/null +++ b/Source/Data/Lang.cs @@ -0,0 +1,9 @@ +using Verse; + +namespace PawnRules.Data +{ + internal static class Lang + { + public static string Get(string key, params object[] args) => (Mod.Id + "." + key).Translate(args); + } +} diff --git a/Source/Data/PawnType.cs b/Source/Data/PawnType.cs new file mode 100644 index 0000000..b5629e5 --- /dev/null +++ b/Source/Data/PawnType.cs @@ -0,0 +1,40 @@ +using System.Linq; +using PawnRules.SDK; + +namespace PawnRules.Data +{ + internal class PawnType : IPresetableType + { + public static readonly PawnType Colonist = new PawnType("Colonist", Lang.Get("PawnType.Colonist"), Lang.Get("PawnType.ColonistPlural"), OptionTarget.Colonist); + public static readonly PawnType Animal = new PawnType("Animal", Lang.Get("PawnType.Animal"), Lang.Get("PawnType.AnimalPlural"), OptionTarget.Animal); + public static readonly PawnType Guest = new PawnType("Guest", Lang.Get("PawnType.Guest"), Lang.Get("PawnType.GuestPlural"), OptionTarget.Guest); + public static readonly PawnType Prisoner = new PawnType("Prisoner", Lang.Get("PawnType.Prisoner"), Lang.Get("PawnType.PrisonerPlural"), OptionTarget.Prisoner); + + public static readonly PawnType[] List = + { + Colonist, + Animal, + Guest, + Prisoner + }; + + public string Id { get; } + public string Label { get; } + public string LabelPlural { get; } + + public OptionTarget AsTarget { get; } + + private PawnType(string id, string label, string labelPlural, OptionTarget target = default(OptionTarget)) + { + Id = id; + Label = label; + LabelPlural = labelPlural; + AsTarget = target; + } + + public bool IsTargetted(OptionTarget target) => (AsTarget != default(OptionTarget)) && ((AsTarget & target) == AsTarget); + + public static PawnType FromTarget(OptionTarget target) => List.FirstOrDefault(type => type.AsTarget == target); + public static PawnType FromId(string id) => List.FirstOrDefault(type => type.Id == id); + } +} diff --git a/Source/Data/Presetable.cs b/Source/Data/Presetable.cs new file mode 100644 index 0000000..b29e1f2 --- /dev/null +++ b/Source/Data/Presetable.cs @@ -0,0 +1,54 @@ +using System; +using System.Text.RegularExpressions; +using Verse; + +namespace PawnRules.Data +{ + internal abstract class Presetable : IExposable, ILoadReferenceable + { + private const int MaxIdLength = 20; + protected static readonly string VoidName = Lang.Get("Preset.None"); + private static readonly Regex NameRegex = new Regex("^(?:[a-zA-Z0-9]|[a-zA-Z0-9]+[a-zA-Z0-9 ]*[a-zA-Z0-9]+)$"); + + private static int _count; + protected readonly int Id; + + public IPresetableType Type { get; protected set; } + public string Name { get; set; } + public bool IsVoid { get; protected set; } + + public bool IsPreset => !Name.NullOrEmpty(); + + protected Presetable() + { + _count++; + Id = _count; + } + + protected Presetable(string name) : this() => Name = name; + + protected abstract string GetPresetId(); + + protected abstract void ExposePresetData(); + + public static bool NameIsValid(IPresetableType type, string name) => (name.Length <= MaxIdLength) && !string.Equals(name, Lang.Get("Preset.None"), StringComparison.OrdinalIgnoreCase) && NameRegex.IsMatch(name) && !Registry.PresetNameExists(type, name); + + public static T CreateVoidPreset(IPresetableType type) where T : Presetable + { + var preset = (T) Activator.CreateInstance(typeof(T), type, VoidName); + preset.IsVoid = true; + return preset; + } + + public static void ResetIds() => _count = 0; + + public void ExposeData() + { + if ((Scribe.mode != LoadSaveMode.Saving) || IsPreset) { Name = ScribePlus.LookValue(Name, "name"); } + ExposePresetData(); + } + + public string GetUniqueLoadID() => $"{Mod.Id}_{GetPresetId()}"; + internal abstract bool IsIgnored(); + } +} diff --git a/Source/Data/Registry.cs b/Source/Data/Registry.cs new file mode 100644 index 0000000..3c92a12 --- /dev/null +++ b/Source/Data/Registry.cs @@ -0,0 +1,297 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using PawnRules.Interface; +using PawnRules.Patch; +using PawnRules.SDK; +using RimWorld; +using RimWorld.Planet; +using Verse; + +namespace PawnRules.Data +{ + internal class Registry : WorldObject + { + private const string WorldObjectDefName = "PawnRules_Registry"; + private const string CurrentVersion = "v" + Mod.Version; + + public static Registry Instance { get; private set; } + public static bool IsActive => !_isDeactivating && (Instance != null) && (Current.Game?.World != null); + + private static bool _isDeactivating; + + private readonly Dictionary> _voidPresets = new Dictionary>(); + private readonly Dictionary>> _presets = new Dictionary>>(); + private readonly Dictionary _defaults = new Dictionary(); + private readonly Dictionary _rules = new Dictionary(); + + private List _savedPresets = new List(); + private List _savedBindings = new List(); + private List _savedDefaults = new List(); + + public static void Initialize() + { + var worldObjects = Current.Game.World.worldObjects; + var instance = (Registry) worldObjects.ObjectsAt(0).FirstOrDefault(worldObject => worldObject is Registry); + + if (instance != null) { return; } + + var def = DefDatabase.GetNamed(WorldObjectDefName); + def.worldObjectClass = typeof(Registry); + instance = (Registry) WorldObjectMaker.MakeWorldObject(def); + def.worldObjectClass = typeof(WorldObject); + instance.Tile = 0; + worldObjects.Add(instance); + } + + public static T GetVoidPreset(IPresetableType type) where T : Presetable => (T) Instance._voidPresets[typeof(T)][type]; + + public static T GetPreset(IPresetableType type, string name) where T : Presetable + { + if (!Instance._presets.ContainsKey(typeof(T)) || !Instance._presets[typeof(T)].ContainsKey(type) || !Instance._presets[typeof(T)][type].ContainsKey(name)) { return null; } + + return (T) Instance._presets[typeof(T)][type][name]; + } + + public static IEnumerable GetPresets(IPresetableType type) where T : Presetable + { + if (!Instance._presets.ContainsKey(typeof(T)) || !Instance._presets[typeof(T)].ContainsKey(type)) { return new T[] { }; } + + return Instance._presets[typeof(T)][type].Values.Cast().ToArray(); + } + + private static void AddPreset(Presetable preset) + { + if (!Instance._presets.ContainsKey(preset.GetType())) { Instance._presets[preset.GetType()] = new Dictionary>(); } + if (!Instance._presets[preset.GetType()].ContainsKey(preset.Type)) { Instance._presets[preset.GetType()][preset.Type] = new Dictionary(); } + + Instance._presets[preset.GetType()][preset.Type][preset.Name] = preset; + } + + public static T CreatePreset(IPresetableType type, string name) where T : Presetable + { + var preset = (T) Activator.CreateInstance(typeof(T), type, name); + AddPreset(preset); + return preset; + } + + public static T RenamePreset(T old, string name) where T : Presetable + { + var preset = Instance._presets[old.GetType()][old.Type][old.Name]; + + Instance._presets[preset.GetType()][preset.Type].Remove(preset.Name); + preset.Name = name; + Instance._presets[preset.GetType()][preset.Type].Add(preset.Name, preset); + + return (T) preset; + } + + public static void DeletePreset(T preset) where T : Presetable + { + if ((preset == null) || preset.IsVoid) { throw new Mod.Exception("Tried to delete void preset"); } + + Instance._presets[preset.GetType()][preset.Type].Remove(preset.Name); + + if (typeof(T) == typeof(Rules)) + { + foreach (var rule in Instance._rules.Where(rule => rule.Value == preset).ToArray()) { Instance._rules[rule.Key] = GetVoidPreset(rule.Value.Type).CloneRulesFor(rule.Key); } + foreach (var rule in Instance._defaults.Where(rule => rule.Value == preset).ToArray()) { Instance._defaults[rule.Key] = GetVoidPreset(rule.Value.Type); } + } + else if (typeof(T) == typeof(Restriction)) + { + foreach (var rulesType in Instance._presets[typeof(Rules)].Values.ToArray()) + { + foreach (var presetable in rulesType.Values.ToArray()) + { + var rule = (Rules) presetable; + var type = (RestrictionType) preset.Type; + if(rule.GetRestriction(type) == preset) { rule.SetRestriction(type, GetVoidPreset(type));} + } + } + } + } + + public static bool PresetNameExists(IPresetableType type, string name) => Instance._presets.ContainsKey(typeof(T)) && Instance._presets[typeof(T)].ContainsKey(type) && Instance._presets[typeof(T)][type].ContainsKey(name); + public static Rules GetDefaultRules(PawnType type) => Instance._defaults[type]; + public static void SetDefaultRules(Rules rules) => Instance._defaults[rules.Type] = rules; + + public static T GetAddonDefaultValue(OptionTarget target, AddonOption addon, T invalidValue = default(T)) + { + var rules = GetDefaultRules(PawnType.FromTarget(target)); + if ((rules == null) || !addon.AllowedInPreset) { return invalidValue; } + return rules.GetAddonValue(addon, invalidValue); + } + + public static Rules GetRules(Pawn pawn) + { + if (!pawn.CanHaveRules()) { return null; } + return Instance._rules.ContainsKey(pawn) ? Instance._rules[pawn] : null; + } + + public static Rules GetOrCreateRules(Pawn pawn) + { + if (!pawn.CanHaveRules()) { return null; } + if (Instance._rules.ContainsKey(pawn)) { return Instance._rules[pawn]; } + + var defaultRules = GetDefaultRules(pawn.GetTargetType()); + var rules = defaultRules.IsVoid ? defaultRules.CloneRulesFor(pawn) : defaultRules; + Instance._rules.Add(pawn, rules); + + return rules; + } + + public static void ReplaceRules(Pawn pawn, Rules rules) => Instance._rules[pawn] = rules; + public static void ReplaceDefaultRules(PawnType type, Rules rules) => Instance._defaults[type] = rules; + + private static Rules ChangeTypeOrCreateRules(Pawn pawn, PawnType type) + { + Instance._rules[pawn] = new Rules(pawn, type ?? pawn.GetTargetType()); + return Instance._rules[pawn]; + } + + public static Rules CloneRules(Pawn original, Pawn cloner) + { + if (!original.CanHaveRules()) { return null; } + if (!Instance._rules.ContainsKey(original)) { return GetOrCreateRules(cloner); } + if (Instance._rules.ContainsKey(cloner)) { DeleteRules(cloner); } + + var cloned = Instance._rules[original].CloneRulesFor(cloner); + Instance._rules.Add(cloner, cloned); + + return cloned; + } + + public static void DeleteRules(Pawn pawn) + { + if ((pawn == null) || !Instance._rules.ContainsKey(pawn)) { return; } + Instance._rules.Remove(pawn); + } + + public static void FactionUpdate(Thing thing, Faction newFaction, bool? guest = null) + { + if (!(thing is Pawn pawn) || !pawn.Spawned || !pawn.Dead) { return; } + + PawnType type; + + if (newFaction == Faction.OfPlayer) + { + if ((guest == null) || pawn.IsColonistPlayerControlled) { type = pawn.RaceProps.Animal ? PawnType.Animal : PawnType.Colonist; } + else { type = guest.Value ? PawnType.Guest : PawnType.Prisoner; } + } + else if ((guest == null ? pawn.Faction : pawn.HostFaction) == Faction.OfPlayer) + { + DeleteRules(pawn); + return; + } + else { return; } + + if (GetDefaultRules(type).IsIgnored()) { return; } + + ChangeTypeOrCreateRules(pawn, type); + } + + public static void DeactivateMod() + { + _isDeactivating = true; + + ModsConfig.SetActive(Mod.ContentPack.Identifier, false); + + var runningMods = PrivateAccess.Verse_LoadedModManager_RunningMods(); + runningMods.Remove(Mod.ContentPack); + + var addonMods = new StringBuilder(); + foreach (var mod in AddonManager.Mods) + { + addonMods.AppendLine(mod.Name); + ModsConfig.SetActive(mod.Identifier, false); + runningMods.Remove(mod); + } + + ModsConfig.Save(); + + if (Find.WorldObjects.Contains(Instance)) { Find.WorldObjects.Remove(Instance); } + + const string saveName = "PawnRules_Removed"; + + GameDataSaveLoader.SaveGame(saveName); + + var message = addonMods.Length > 0 ? Lang.Get("Button.RemoveModAndAddonsComplete", saveName.Bold(), addonMods.ToString()) : Lang.Get("Button.RemoveModComplete", saveName.Bold()); + Find.WindowStack.Add(new Dialog_Alert(message, Dialog_Alert.Buttons.Ok, GenCommandLine.Restart)); + } + + public override void PostAdd() + { + base.PostAdd(); + Instance = this; + InitVoids(); + InitDefaults(); + } + + private void InitVoids() + { + _voidPresets[typeof(Rules)] = new Dictionary(); + foreach (var type in PawnType.List) { _voidPresets[typeof(Rules)][type] = Presetable.CreateVoidPreset(type); } + + _voidPresets[typeof(Restriction)] = new Dictionary(); + foreach (var type in RestrictionType.List) { _voidPresets[typeof(Restriction)][type] = Presetable.CreateVoidPreset(type); } + } + + private void InitDefaults() + { + foreach (var type in PawnType.List.Where(type => !_defaults.ContainsKey(type))) { _defaults.Add(type, (Rules) _voidPresets[typeof(Rules)][type]); } + } + + public override void ExposeData() + { + if (_isDeactivating) { return; } + base.ExposeData(); + + if (Scribe.mode == LoadSaveMode.LoadingVars) + { + Instance = this; + InitVoids(); + } + + var version = CurrentVersion; + Scribe_Values.Look(ref version, "version"); + + if (Scribe.mode == LoadSaveMode.Saving) + { + _savedPresets.Clear(); + _savedDefaults.Clear(); + _savedBindings.Clear(); + + foreach (var classType in _presets.Values) + { + foreach (var type in classType.Values) { _savedPresets.AddRange(type.Values.ToArray()); } + } + + _savedDefaults.AddRange(_defaults.Values.Where(preset => !preset.IsVoid).Select(preset => new Binding(preset.Type, preset)).ToArray()); + _savedBindings.AddRange(_rules.Where(rules => rules.Key.CanHaveRules()).Select(rules => new Binding(rules.Key, rules.Value.IsIgnored() ? null : rules.Value)).ToArray()); + } + + Scribe_Collections.Look(ref _savedPresets, "presets", LookMode.Deep); + Scribe_Collections.Look(ref _savedBindings, "bindings", LookMode.Deep); + Scribe_Collections.Look(ref _savedDefaults, "defaults", LookMode.Deep); + + if (Scribe.mode != LoadSaveMode.PostLoadInit) { return; } + + _presets.Clear(); + _defaults.Clear(); + _rules.Clear(); + + foreach (var preset in _savedPresets) { AddPreset(preset); } + + foreach (var preset in _savedDefaults) { _defaults.Add(preset.Target, preset.Rules); } + InitDefaults(); + + foreach (var binding in _savedBindings.Where(binding => binding.Pawn.CanHaveRules())) { _rules.Add(binding.Pawn, binding.Rules); } + + _savedPresets.Clear(); + _savedDefaults.Clear(); + _savedBindings.Clear(); + Presetable.ResetIds(); + } + } +} diff --git a/Source/Data/Restriction.cs b/Source/Data/Restriction.cs new file mode 100644 index 0000000..50e5102 --- /dev/null +++ b/Source/Data/Restriction.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Linq; +using Verse; + +namespace PawnRules.Data +{ + internal class Restriction : Presetable + { + //public static readonly Restriction None = new Restriction(null, Lang.Get("Preset.None")); + public new RestrictionType Type { get => (RestrictionType) base.Type; private set => base.Type = value; } + + private List _defs = new List(); + + public Restriction() + { } + + public Restriction(RestrictionType type, string name) : base(name) => Type = type; + + public Restriction GetRenamed(string name) => new Restriction(Type, name) { _defs = _defs }; + + public bool Matches(RestrictionTemplate template) => _defs.SequenceEqual(from category in template.Categories from member in category.Members where !member.Value select member.Def.defName); + + public void Update(RestrictionTemplate template) + { + _defs.Clear(); + _defs.AddRange((from category in template.Categories from member in category.Members where !member.Value select member.Def.defName).ToList()); + } + + public bool Allows(Def def) => !_defs.Contains(def.defName); + + protected override void ExposePresetData() + { + Type = RestrictionType.FromId(ScribePlus.LookValue(Type?.Id, "type")); + Scribe_Collections.Look(ref _defs, "restricted", LookMode.Value); + } + + internal override bool IsIgnored() => _defs.Count == 0; + + protected override string GetPresetId() => $"Restriction_{Type.Id}_{Name}"; + } +} diff --git a/Source/Data/RestrictionTemplate.cs b/Source/Data/RestrictionTemplate.cs new file mode 100644 index 0000000..3a4ec7e --- /dev/null +++ b/Source/Data/RestrictionTemplate.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using PawnRules.Patch; +using Verse; + +namespace PawnRules.Data +{ + internal class RestrictionTemplate + { + private static readonly PawnKindDef[] AnimalCache = DefDatabase.AllDefs.Where(kindDef => kindDef.RaceProps.Animal).OrderBy(animal => animal.label).ToArray(); + private static readonly ThingDef[] FoodCache = DefDatabase.AllDefs.Where(food => food.IsNutritionGivingIngestible && !food.IsDrug).ToList().OrderBy(food => food.category).ThenBy(food => food.FirstThingCategory?.index).ThenBy(food => food.label).ToArray(); + public readonly IEnumerable Categories; + + public RestrictionTemplate(IEnumerable list) => Categories = list; + + public void ToggleAll(bool value) + { + foreach (var category in Categories) + { + foreach (var member in category.Members) { member.Value = value; } + } + } + + private static RestrictionTemplate GetFoodsCategorized(Restriction restriction) + { + var list = new Dictionary(); + + foreach (var food in FoodCache) + { + var category = food.GetCategoryLabel(); + + if (!list.ContainsKey(category)) { list[category] = new Category(category); } + list[category].Members.Add(new Toggle(food, restriction.Allows(food))); + } + + return new RestrictionTemplate(list.Values.ToArray()); + } + + private static RestrictionTemplate GetAnimalsCategorized(Restriction restriction) + { + var list = new Dictionary(); + + var categories = new[] + { + Lang.Get("AnimalCategory.Pet"), + Lang.Get("AnimalCategory.Farm"), + Lang.Get("AnimalCategory.Herd"), + Lang.Get("AnimalCategory.Predator"), + Lang.Get("AnimalCategory.Insect"), + Lang.Get("AnimalCategory.Other") + }; + + foreach (var animal in AnimalCache) + { + string category; + + if (animal.RaceProps.petness > 0.5f) { category = categories[0]; } + else if (animal.race.tradeTags.Contains("AnimalFarm")) { category = categories[1]; } + else if (animal.RaceProps.herdAnimal) { category = categories[2]; } + else if (animal.RaceProps.predator) { category = categories[3]; } + else if (animal.race.tradeTags.Contains("AnimalInsect")) { category = categories[4]; } + else { category = categories[5]; } + + if (!list.ContainsKey(category)) { list[category] = new Category(category); } + list[category].Members.Add(new Toggle(animal, restriction.Allows(animal))); + } + + return new RestrictionTemplate(list.Values.OrderBy(member => Array.IndexOf(categories, member.Label)).ToArray()); + } + + public static RestrictionTemplate Build(RestrictionType type, Restriction restriction) + { + if (type == RestrictionType.Food) { return GetFoodsCategorized(restriction); } + return type == RestrictionType.Bonding ? GetAnimalsCategorized(restriction) : null; + } + + public class Category + { + public string Label { get; } + public List Members { get; } = new List(); + + public Category(string label) => Label = label; + + public MultiCheckboxState GetListState() + { + if (Members.Count < 1) { return MultiCheckboxState.Off; } + var first = Members.First().Value; + return Members.All(q => q.Value == first) ? (first ? MultiCheckboxState.On : MultiCheckboxState.Off) : MultiCheckboxState.Partial; + } + + public void UpdateState(MultiCheckboxState state) + { + if (state == MultiCheckboxState.Off) { SetAll(false); } + else if (state == MultiCheckboxState.On) { SetAll(true); } + } + + private void SetAll(bool state) + { + foreach (var member in Members) { member.Value = state; } + } + } + } +} diff --git a/Source/Data/RestrictionType.cs b/Source/Data/RestrictionType.cs new file mode 100644 index 0000000..4d44959 --- /dev/null +++ b/Source/Data/RestrictionType.cs @@ -0,0 +1,31 @@ +using System.Linq; + +namespace PawnRules.Data +{ + internal class RestrictionType : IPresetableType + { + public static readonly RestrictionType Food = new RestrictionType("Food", Lang.Get("RestrictionType.Food"), Lang.Get("RestrictionType.FoodPlural"), Lang.Get("RestrictionType.FoodCategorization")); + public static readonly RestrictionType Bonding = new RestrictionType("Bonding", Lang.Get("RestrictionType.Bonding"), Lang.Get("RestrictionType.BondingPlural"), Lang.Get("RestrictionType.BondingCategorization")); + + public static readonly RestrictionType[] List = + { + Food, + Bonding + }; + + public string Id { get; } + public string Label { get; } + public string LabelPlural { get; } + public string Categorization { get; } + + private RestrictionType(string id, string label, string labelPlural, string categorization) + { + Id = id; + Label = label; + LabelPlural = labelPlural; + Categorization = categorization; + } + + public static RestrictionType FromId(string id) => List.FirstOrDefault(type => type.Id == id); + } +} diff --git a/Source/Data/Rules.cs b/Source/Data/Rules.cs new file mode 100644 index 0000000..9b149d3 --- /dev/null +++ b/Source/Data/Rules.cs @@ -0,0 +1,157 @@ +using System.Collections.Generic; +using System.Linq; +using PawnRules.Patch; +using Verse; + +namespace PawnRules.Data +{ + internal class Rules : Presetable + { + public Pawn Pawn { get; private set; } + public new PawnType Type { get => (PawnType) base.Type; private set => base.Type = value; } + + private readonly Dictionary _restrictions = new Dictionary(); + private readonly Dictionary _addonValues = new Dictionary(); + + public bool AllowCourting = true; + public bool AllowArtisan = true; + + public bool HasAddons { get; private set; } + public float AddonsRectHeight { get; private set; } + + public Rules() + { } + + public Rules(Pawn pawn, PawnType type = null) + { + Pawn = pawn; + Type = type ?? pawn.GetTargetType(); + } + + public Rules(PawnType type, string name) : base(name) => Type = type; + + public void CopyRules(Rules rules) + { + Type = rules.Type; + + CopyRestrictions(rules._restrictions); + + AllowCourting = rules.AllowCourting; + AllowArtisan = rules.AllowArtisan; + + if (!rules.HasAddons) { return; } + _addonValues.Clear(); + InitAddons(); + + foreach (var addon in rules._addonValues.Keys.ToArray()) { _addonValues[addon] = rules._addonValues[addon]; } + } + + private void CopyRestrictions(IDictionary from) + { + foreach (var type in from.Keys.ToArray()) { _restrictions[type] = from[type]; } + } + + public Rules CloneRulesFor(Pawn pawn) + { + var rules = new Rules(pawn); + rules.CopyRules(this); + return rules; + } + + public Rules ClonePreset(string name = null) + { + var rules = new Rules(Type, name ?? Name); + rules.CopyRules(this); + return rules; + } + + private void InitAddons() + { + if (!AddonManager.HasAddons || HasAddons) { return; } + + foreach (var option in AddonManager.Options) + { + if (!Type.IsTargetted(option.Target)) { continue; } + _addonValues.Add(option, option.DefaultValue); + AddonsRectHeight += option.WidgetHeight; + } + + HasAddons = _addonValues.Count > 0; + if (HasAddons) { AddonsRectHeight += 12f; } + } + + public IEnumerable GetAddons() => _addonValues.Keys.ToArray(); + + public bool HasAddon(AddonOption addon) => HasAddons && _addonValues.ContainsKey(addon); + + public T GetAddonValue(AddonOption addon, T invalidValue) => _addonValues.ContainsKey(addon) ? (T) _addonValues[addon] : invalidValue; + + public bool SetAddonValueDirect(AddonOption addon, object value) + { + if (!_addonValues.ContainsKey(addon)) { return false; } + + _addonValues[addon] = value; + return true; + } + + public void SetToVanilla() + { + foreach (var type in _restrictions.Keys.ToArray()) { _restrictions[type] = Registry.GetVoidPreset(type); } + + AllowCourting = true; + AllowArtisan = true; + + foreach (var addon in _addonValues.Keys.ToArray()) { _addonValues[addon] = addon.DefaultValue; } + } + + internal override bool IsIgnored() => _restrictions.Values.All(restrictions => restrictions.IsVoid) && AllowCourting && AllowArtisan && (!AddonManager.HasAddons || _addonValues.All(addon => addon.Value == addon.Key.DefaultValue)); + + public Restriction GetRestriction(RestrictionType type) => _restrictions.ContainsKey(type) ? _restrictions[type] : Registry.GetVoidPreset(type); + + public void SetRestriction(RestrictionType type, Restriction restriction) => _restrictions[type] = restriction; + + public void SetPawn(Pawn pawn) + { + Pawn = pawn; + Type = pawn.GetTargetType(); + } + + private void ExposeAddon(AddonOption addon) + { + var oldValue = (T) _addonValues[addon]; + var newValue = ScribePlus.LookValue(oldValue, addon.Key, (T) addon.DefaultValue); + + if ((Scribe.mode == LoadSaveMode.Saving) || Equals(newValue, oldValue)) { return; } + + SetAddonValueDirect(addon, newValue); + } + + protected override void ExposePresetData() + { + if ((Scribe.mode != LoadSaveMode.Saving) || IsPreset) { Type = PawnType.FromId(ScribePlus.LookValue(Type?.Id, "type")); } + + if (Scribe.mode == LoadSaveMode.LoadingVars) { InitAddons(); } + + foreach (var type in RestrictionType.List) + { + var restriction = GetRestriction(type); + if ((Scribe.mode != LoadSaveMode.Saving) || !restriction.IsVoid) { SetRestriction(type, ScribePlus.LookReference(restriction, type.Id.ToLower(), Registry.GetVoidPreset(type))); } + } + + AllowCourting = ScribePlus.LookValue(AllowCourting, "courting", true); + AllowArtisan = ScribePlus.LookValue(AllowArtisan, "artisan", true); + + if (!HasAddons) { return; } + + foreach (var addon in _addonValues.Keys.ToArray()) + { + if (addon.Type == typeof(string)) { ExposeAddon(addon); } + else if (addon.Type == typeof(bool)) { ExposeAddon(addon); } + else if (addon.Type == typeof(int)) { ExposeAddon(addon); } + else if (addon.Type == typeof(float)) { ExposeAddon(addon); } + } + } + + protected override string GetPresetId() => $"Rules_{Type?.Id ?? "Binding"}_{Pawn?.GetUniqueLoadID() ?? Name ?? Id.ToString()}"; + } +} diff --git a/Source/Data/ScribePlus.cs b/Source/Data/ScribePlus.cs new file mode 100644 index 0000000..33f70b4 --- /dev/null +++ b/Source/Data/ScribePlus.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using Verse; + +namespace PawnRules.Data +{ + internal static class ScribePlus + { + public static List LookList(List list, string key, bool ignoreIfEmpty = true) + { + if ((Scribe.mode == LoadSaveMode.Saving) && !ignoreIfEmpty && ((list == null) || (list.Count == 0))) { return list; } + Scribe_Collections.Look(ref list, key, LookMode.Deep); + + return list ?? new List(); + } + + public static T LookReference(T reference, string key, T defaultValue = null) where T : class, ILoadReferenceable + { + if ((Scribe.mode == LoadSaveMode.Saving) && Equals(reference, defaultValue)) { return reference; } + Scribe_References.Look(ref reference, key); + + return reference ?? defaultValue; + } + + public static T LookValue(T value, string key, T defaultValue = default(T)) + { + Scribe_Values.Look(ref value, key, defaultValue); + return value; + } + } +} diff --git a/Source/Data/Toggle.cs b/Source/Data/Toggle.cs new file mode 100644 index 0000000..0db65da --- /dev/null +++ b/Source/Data/Toggle.cs @@ -0,0 +1,17 @@ +using Verse; + +namespace PawnRules.Data +{ + internal class Toggle + { + public Def Def { get; } + + public bool Value; + + public Toggle(Def def, bool value) + { + Def = def; + Value = value; + } + } +} diff --git a/Source/Interface/Dialog_Alert.cs b/Source/Interface/Dialog_Alert.cs new file mode 100644 index 0000000..2206ce5 --- /dev/null +++ b/Source/Interface/Dialog_Alert.cs @@ -0,0 +1,78 @@ +using System; +using PawnRules.Data; +using PawnRules.Patch; +using UnityEngine; +using Verse; + +namespace PawnRules.Interface +{ + internal class Dialog_Alert : Window + { + public override Vector2 InitialSize { get; } + private readonly string _message; + private readonly Buttons _buttons; + private readonly Action _onAccept; + private readonly Action _onCancel; + private bool _isAccepted; + + public Dialog_Alert(string message, Buttons buttons = Buttons.Ok, Action onAccept = null, Action onCancel = null) + { + doCloseButton = false; + closeOnAccept = true; + closeOnClickedOutside = false; + absorbInputAroundWindow = true; + + _message = message; + _buttons = buttons; + _onAccept = onAccept; + _onCancel = onCancel; + + var wrap = Text.WordWrap; + Text.WordWrap = true; + InitialSize = new Vector2(400f, 72f + Text.CalcHeight(_message, 364f)); + Text.WordWrap = wrap; + } + + public override void DoWindowContents(Rect rect) + { + var listing = new Listing_Standard(); + var vGrid = rect.GetVGrid(4f, 0f, 30f); + + listing.Begin(vGrid[0]); + listing.Label(_message); + listing.End(); + + var hGrid = vGrid[1].GetHGrid(4f, 100f, 0f); + + listing.Begin(_buttons == Buttons.Ok ? vGrid[1] : hGrid[0]); + + if (listing.ButtonText(_buttons == Buttons.YesNo ? Lang.Get("Button.Yes") : Lang.Get("Button.OK"))) + { + _isAccepted = true; + _onAccept?.Invoke(); + Close(); + } + + listing.End(); + + if (_buttons == Buttons.Ok) { return; } + + listing.Begin(hGrid[1]); + if (listing.ButtonText(_buttons == Buttons.YesNo ? Lang.Get("Button.No") : Lang.Get("Button.Cancel"))) { Close(); } + listing.End(); + } + + public override void Close(bool doCloseSound = true) + { + if (!_isAccepted) { _onCancel?.Invoke(); } + base.Close(doCloseSound); + } + + public enum Buttons + { + Ok, + OkCancel, + YesNo + } + } +} diff --git a/Source/Interface/Dialog_Global.cs b/Source/Interface/Dialog_Global.cs new file mode 100644 index 0000000..3bae94e --- /dev/null +++ b/Source/Interface/Dialog_Global.cs @@ -0,0 +1,20 @@ +using PawnRules.Data; +using UnityEngine; +using Verse; + +namespace PawnRules.Interface +{ + internal class Dialog_Global : WindowPlus + { + public Dialog_Global() : base(Lang.Get("Dialog_Global.Title"), new Vector2(300f, 300)) + { } + + public override void DoContent(Rect rect) + { + var listing = new Listing_Standard(); + listing.Begin(rect); + if (listing.ButtonText(Lang.Get("Button.RemoveMod"), Lang.Get("Button.RemoveModDesc"))) { Find.WindowStack.Add(new Dialog_Alert(Lang.Get("Button.RemoveModConfirm"), Dialog_Alert.Buttons.YesNo, Registry.DeactivateMod)); } + listing.End(); + } + } +} diff --git a/Source/Interface/Dialog_PresetName.cs b/Source/Interface/Dialog_PresetName.cs new file mode 100644 index 0000000..3be4f43 --- /dev/null +++ b/Source/Interface/Dialog_PresetName.cs @@ -0,0 +1,91 @@ +using System; +using PawnRules.Data; +using PawnRules.Patch; +using UnityEngine; +using Verse; + +namespace PawnRules.Interface +{ + internal class Dialog_PresetName : WindowPlus where T : Presetable + { + private string _name; + + private bool _focused; + private readonly T _preset; + private readonly IPresetableType _type; + private readonly Action _onCommit; + + public Dialog_PresetName(string title) : base(title, new Vector2(400f, 170f)) + { + doCloseButton = false; + closeOnClickedOutside = false; + closeOnAccept = true; + } + + public Dialog_PresetName(IPresetableType type, Action onCommit) : this(Lang.Get("Dialog_PresetName.TitleNew")) + { + _type = type; + _onCommit = onCommit; + } + + public Dialog_PresetName(T preset, Action onCommit) : this(Lang.Get("Dialog_PresetName.Title", preset.Name)) + { + _preset = preset; + _name = _preset.Name; + _onCommit = onCommit; + } + + public override void OnAcceptKeyPressed() + { + if (!NameIsValid()) { return; } + CommitName(); + base.OnAcceptKeyPressed(); + } + + private bool NameIsValid() => Presetable.NameIsValid(_type ?? _preset.Type, _name); + + private void CommitName() + { + if (_type != null) + { + var preset = Registry.CreatePreset(_type, _name); + _onCommit(preset); + return; + } + + _onCommit(Registry.RenamePreset(_preset, _name)); + } + + public override void DoContent(Rect rect) + { + var listing = new Listing_StandardPlus(); + + listing.Begin(rect); + listing.Label(Lang.Get("Dialog_PresetName.Label")); + GUI.SetNextControlName("RenameField"); + _name = listing.TextEntry(_name); + var valid = NameIsValid(); + if (!_focused) + { + UI.FocusControl("RenameField", this); + _focused = true; + } + listing.Gap(); + listing.End(); + + var grid = rect.AdjustedBy(0f, listing.CurHeight, 0f, -listing.CurHeight).GetHGrid(4f, 0f, 0f); + + listing.Begin(grid[0]); + if (listing.ButtonText(Lang.Get("Button.OK"), null, valid)) + { + CommitName(); + Close(); + } + listing.End(); + + listing.Begin(grid[1]); + if (listing.ButtonText(Lang.Get("Button.Cancel"))) { Close(); } + listing.End(); + } + } +} diff --git a/Source/Interface/Dialog_Restrictions.cs b/Source/Interface/Dialog_Restrictions.cs new file mode 100644 index 0000000..20936d6 --- /dev/null +++ b/Source/Interface/Dialog_Restrictions.cs @@ -0,0 +1,172 @@ +using PawnRules.Data; +using PawnRules.Patch; +using UnityEngine; +using Verse; + +namespace PawnRules.Interface +{ + internal class Dialog_Restrictions : WindowPlus + { + private readonly RestrictionType _type; + private readonly Rules _rules; + private RestrictionTemplate _template; + + private Color _color; + + public override Vector2 InitialSize => new Vector2(800f, 600f); + + private readonly Listing_StandardPlus _headerList = new Listing_StandardPlus(); + private readonly Listing_StandardPlus _categoryList = new Listing_StandardPlus(); + private readonly Listing_StandardPlus _membersList = new Listing_StandardPlus(); + private readonly Listing_Preset _presetList; + + public Dialog_Restrictions(RestrictionType type, Rules rules) + { + _rules = rules; + _type = type; + + _presetList = new Listing_Preset(_type, _rules.GetRestriction(_type), new[] { Registry.GetVoidPreset(_type) }, RefreshTemplate, SaveTemplate, null); + RefreshTemplate(); + } + + private void RefreshTemplate() + { + _template = RestrictionTemplate.Build(_type, _presetList.Selected); + _rules.SetRestriction(_type, _presetList.Selected); + } + + private void SaveTemplate() => _presetList.Selected.Update(_template); + + private bool HasMadeChanges() => !_presetList.Selected.IsVoid && !_rules.GetRestriction(_presetList.Selected.Type).Matches(_template); + + public override void Close(bool doCloseSound = true) + { + if (_presetList.EditMode && (_presetList.IsUnsaved || HasMadeChanges())) + { + void OnAccept() + { + _presetList.Selected.Update(_template); + RefreshTemplate(); + _presetList.Commit(); + base.Close(doCloseSound); + } + + void OnCancel() + { + if (_presetList.IsUnsaved) + { + Registry.DeletePreset(_presetList.Selected); + _presetList.Revert(); + } + + base.Close(doCloseSound); + } + + Find.WindowStack.Add(new Dialog_Alert(Lang.Get("Button.PresetSaveConfirm"), Dialog_Alert.Buttons.YesNo, OnAccept, OnCancel)); + return; + } + + base.Close(doCloseSound); + } + + public override void DoContent(Rect rect) + { + Title = _presetList.EditMode || (_rules.Pawn == null) ? Lang.Get("Dialog_Restrictions.TitlePreset", _type.Label, _presetList.Selected.Name.Bold()) : Lang.Get("Dialog_Restrictions.TitlePawn", _type.Label, _rules.Pawn.Name.ToStringFull.Bold(), _rules.Type.Label); + + _color = GUI.color; + + var vGrid = rect.GetVGrid(4f, 42f, 0f); + var hGrid = vGrid[1].GetHGrid(8f, 200f, 0f, 0f); + DoHeader(vGrid[0]); + + _presetList.DoContent(hGrid[0]); + DoCategories(hGrid[1]); + DoMembers(hGrid[2]); + } + + private void DoHeader(Rect rect) + { + var grid = rect.GetHGrid(8f, 200f, 0f, 0f); + _headerList.Begin(grid[0]); + _headerList.Label(Lang.Get("Preset.Header").Italic().Bold()); + _headerList.GapLine(); + _headerList.End(); + + _headerList.Begin(grid[1]); + _headerList.Label(Lang.Get("Dialog_Restrictions.HeaderCategory").Italic().Bold()); + _headerList.GapLine(); + _headerList.End(); + + _headerList.Begin(grid[2]); + _headerList.Label(_type.Categorization.Italic().Bold()); + _headerList.GapLine(); + _headerList.End(); + } + + private void DoCategories(Rect rect) + { + var vGrid = rect.GetVGrid(4f, 0f, 30f); + _categoryList.Begin(vGrid[0]); + + foreach (var category in _template.Categories) + { + var state = category.GetListState(); + + if (_presetList.EditMode) + { + _categoryList.CheckboxPartial(category.Label, ref state); + category.UpdateState(state); + continue; + } + + if ((_type == RestrictionType.Food) && (_rules.Pawn != null) && !category.Members.Any(member => _rules.Pawn.RaceProps.CanEverEat((ThingDef) member.Def))) { continue; } + + GUI.color = GuiPlus.ReadOnlyColor; + _categoryList.CheckboxPartial(category.Label, ref state); + GUI.color = _color; + } + _categoryList.End(); + + if (!_presetList.EditMode) { return; } + + var hGrid = vGrid[1].GetHGrid(4f, 0f, 0f); + _categoryList.Begin(hGrid[0]); + if (_categoryList.ButtonText(Lang.Get("Button.RestrictionsAllowOn"))) { _template.ToggleAll(true); } + _categoryList.End(); + _categoryList.Begin(hGrid[1]); + if (_categoryList.ButtonText(Lang.Get("Button.RestrictionsAllowOff"))) { _template.ToggleAll(false); } + _categoryList.End(); + } + + private void DoMembers(Rect rect) + { + _membersList.Begin(rect, true); + + foreach (var category in _template.Categories) + { + if ((_type == RestrictionType.Food) && (_rules.Pawn != null) && !category.Members.Any(member => _rules.Pawn.RaceProps.CanEverEat((ThingDef) member.Def))) { continue; } + + _membersList.LabelTiny(category.Label.Bold()); + + foreach (var member in category.Members) + { + if (_presetList.EditMode) + { + _membersList.CheckboxLabeled(member.Def.LabelCap, ref member.Value, member.Def.description); + continue; + } + + if ((_type == RestrictionType.Food) && (_rules.Pawn != null) && !_rules.Pawn.RaceProps.CanEverEat((ThingDef) member.Def)) { continue; } + + GUI.color = GuiPlus.ReadOnlyColor; + _membersList.CheckboxLabeled(member.Def.LabelCap, ref member.Value, member.Def.description, false); + GUI.color = _color; + } + + _membersList.Gap(); + } + + _membersList.End(); + } + } +} diff --git a/Source/Interface/Dialog_Rules.cs b/Source/Interface/Dialog_Rules.cs new file mode 100644 index 0000000..e3d1fe4 --- /dev/null +++ b/Source/Interface/Dialog_Rules.cs @@ -0,0 +1,257 @@ +using System.Collections.Generic; +using System.Linq; +using PawnRules.Data; +using PawnRules.Patch; +using UnityEngine; +using Verse; + +namespace PawnRules.Interface +{ + internal class Dialog_Rules : WindowPlus + { + private const float OptionButtonSize = 80f; + + private readonly Pawn _pawn; + + private readonly Listing_Preset _preset; + private readonly Listing_StandardPlus _addons = new Listing_StandardPlus(); + + private readonly List _floatMenuViews = new List(); + private List _floatMenuAssign; + + private PawnType _type; + private Rules _template; + private Rules _personalized; + + private Dialog_Rules(Pawn pawn, Rules rules) : base(new Vector2(700f, 600f)) + { + _pawn = pawn; + + _personalized = rules.CloneRulesFor(_pawn); + _template = rules.IsPreset ? rules.ClonePreset() : _personalized; + + _preset = new Listing_Preset(rules.Type, rules.IsPreset ? rules : _personalized, new[] { _personalized }, UpdateSelected, CommitTemplate, RevertTemplate); + + if (_pawn != null) { _floatMenuViews.Add(new FloatMenuOption(Lang.Get("PawnType.Individual"), () => ChangeType(null))); } + foreach (var type in PawnType.List) { _floatMenuViews.Add(new FloatMenuOption(Lang.Get("Dialog_Rules.DefaultType", type.Label), () => ChangeType(type))); } + + _floatMenuAssign = GetAssignmentOptions(); + } + + public static void OpenFromPawn(Pawn pawn) => Find.WindowStack.Add(new Dialog_Rules(pawn, Registry.GetOrCreateRules(pawn))); + + private void ChangeType(PawnType type) + { + _preset.Type = type ?? _pawn.GetTargetType(); + _type = type; + _preset.FixedPresets = _type == null ? new[] { _personalized } : new[] { Registry.GetVoidPreset(_template.Type) }; + if (type == null) + { + var rules = Registry.GetOrCreateRules(_pawn); + _preset.Selected = rules.IsPreset ? rules : _personalized; + } + else { _preset.Selected = Registry.GetDefaultRules(type); } + + UpdateTemplate(); + } + + private void ChangeRestriction(RestrictionType type) + { + var list = new List(); + + var presets = Registry.GetPresets(type).Where(preset => preset != _template.GetRestriction(type)); + + if (!presets.Any() && _template.GetRestriction(type).IsVoid) + { + Find.WindowStack.Add(new Dialog_Restrictions(type, _template)); + return; + } + + list.Add(new FloatMenuOption(Lang.Get("Dialog_Rules.EditRestriction"), () => Find.WindowStack.Add(new Dialog_Restrictions(type, _template)))); + + var voidPreset = Registry.GetVoidPreset(type); + if (!_template.GetRestriction(type).IsVoid) { list.Add(new FloatMenuOption(Lang.Get("Dialog_Rules.ClearRestriction", voidPreset.Type.Categorization.ToLower()), () => _template.SetRestriction(type, voidPreset))); } + list.AddRange(presets.Select(restriction => new FloatMenuOption(Lang.Get("Dialog_Rules.ChangeRestriction", restriction.Name.Bold()), () => _template.SetRestriction(type, restriction)))); + + Find.WindowStack.Add(new FloatMenu(list)); + } + + private void CommitTemplate() + { + if (_preset.Selected == _personalized) + { + Find.WindowStack.Add(new Dialog_PresetName(_preset.Type, rules => + { + rules.CopyRules(_personalized); + _preset.Selected = rules; + UpdateSelected(); + })); + return; + } + + _preset.Selected.CopyRules(_template); + UpdateTemplate(); + } + + private void UpdateTemplate() + { + if (_preset.Selected.IsVoid && (_type == null)) + { + _personalized = _preset.Selected.CloneRulesFor(_pawn); + _preset.FixedPresets = new[] { _personalized }; + _preset.Selected = _personalized; + Registry.ReplaceRules(_pawn, _preset.Selected); + } + + _template = _preset.Selected.IsPreset ? _preset.Selected.ClonePreset() : _personalized; + _floatMenuAssign = GetAssignmentOptions(); + } + + private void RevertTemplate() + { + if (_preset.Selected == _personalized) { _personalized.SetToVanilla(); } + else { UpdateTemplate(); } + } + + private void UpdateSelected() + { + if (_type == null) { Registry.ReplaceRules(_pawn, _preset.Selected); } + else { Registry.ReplaceDefaultRules(_type, _preset.Selected); } + + UpdateTemplate(); + } + + private IEnumerable GetOtherPawnsOfType(bool byKind) => _type == null ? Find.CurrentMap.mapPawns.AllPawns.Where(pawn => (pawn != _pawn) && (pawn.GetTargetType() == _preset.Type) && (!byKind || (pawn.kindDef == _pawn.kindDef))) : Find.CurrentMap.mapPawns.AllPawns.Where(pawn => pawn.GetTargetType() == _type); + + private string GetPresetNameDefinite() + { + if (_preset.Selected == _personalized) { return Lang.Get("Dialog_Rules.AssignPersonalizedName"); } + return _preset.Selected.IsVoid ? Lang.Get("Dialog_Rules.AssignVoidName") : Lang.Get("Dialog_Rules.AssignSpecificName", _preset.Selected.Name.Bold()); + } + + private List GetAssignmentOptions() + { + var options = new List(); + + var otherPawnsOfType = GetOtherPawnsOfType(false); + if (GetOtherPawnsOfType(false).Any()) { options.Add(new FloatMenuOption(Lang.Get("Dialog_Rules.AssignAll", _preset.Type.LabelPlural.ToLower()), () => AssignAll(false))); } + if ((_type == null) && _pawn.RaceProps.Animal && otherPawnsOfType.Any(kind => kind.kindDef == _pawn.kindDef)) { options.Add(new FloatMenuOption(Lang.Get("Dialog_Rules.AssignAll", _pawn.kindDef.GetLabelPlural().ToLower()), () => AssignAll(true))); } + options.AddRange(Find.CurrentMap.mapPawns.AllPawns.Where(pawn => ((_type != null) || (pawn != _pawn)) && (pawn.GetTargetType() == _preset.Type)).Select(pawn => new FloatMenuOption(Lang.Get("Dialog_Rules.AssignSpecific", pawn.Name.ToString().Italic()), () => AssignSpecific(pawn)))); + if ((_type == null) && _preset.Selected.IsPreset) { options.Add(new FloatMenuOption(Lang.Get("Dialog_Rules.AssignDefault", _preset.Type.LabelPlural.ToLower()), () => Find.WindowStack.Add(new Dialog_Alert(Lang.Get("Dialog_Rules.AssignDefaultConfirm", _preset.Type.LabelPlural.ToLower(), _preset.Selected.Name.Bold()), Dialog_Alert.Buttons.YesNo, () => Registry.SetDefaultRules(_preset.Selected))))); } + + return options; + } + + private void AssignAll(bool byKind) + { + var pawns = GetOtherPawnsOfType(byKind); + + void OnAccept() + { + foreach (var pawn in pawns) { Registry.ReplaceRules(pawn, _preset.Selected.IsPreset ? _preset.Selected : _template.ClonePreset()); } + } + + var count = pawns.Count(); + Find.WindowStack.Add(new Dialog_Alert(Lang.Get("Dialog_Rules.AssignAllConfirm", GetPresetNameDefinite(), count.ToString().Bold(), byKind ? _pawn.kindDef.GetLabelPlural(count) : count > 1 ? _preset.Selected.Type.LabelPlural : _preset.Selected.Type.Label), Dialog_Alert.Buttons.YesNo, OnAccept)); + } + + private void AssignSpecific(Pawn pawn) + { + void OnAccept() => Registry.ReplaceRules(pawn, _preset.Selected.IsPreset ? _preset.Selected : _template.ClonePreset()); + + Find.WindowStack.Add(new Dialog_Alert(Lang.Get("Dialog_Rules.AssignSpecificConfirm", GetPresetNameDefinite(), pawn.Name.ToString().Italic()), Dialog_Alert.Buttons.YesNo, OnAccept)); + } + + public override void Close(bool doCloseSound = true) + { + if (_preset.EditMode) + { + void OnAccept() + { + CommitTemplate(); + base.Close(doCloseSound); + } + + void OnCancel() + { + _preset.Revert(); + base.Close(doCloseSound); + } + + Find.WindowStack.Add(new Dialog_Alert(Lang.Get("Button.PresetSaveConfirm"), Dialog_Alert.Buttons.YesNo, OnAccept, OnCancel)); + return; + } + + if (_preset.Selected == _personalized) { Registry.ReplaceRules(_pawn, _personalized); } + base.Close(doCloseSound); + } + + public override void DoContent(Rect rect) + { + if (!Registry.IsActive) + { + Close(); + return; + } + + Title = _type == null ? Lang.Get("Dialog_Rules.Title", _pawn.Name.ToStringFull.Bold(), _preset.Type.Label) : Lang.Get("Dialog_Rules.TitleDefault", _type.LabelPlural.Bold()); + + var listing = new Listing_StandardPlus(); + var hGrid = rect.GetHGrid(8f, 200f, 0f); + var lGrid = hGrid[0].GetVGrid(4f, 42f, 0f); + + listing.Begin(lGrid[0]); + listing.Label(Lang.Get("Preset.Header").Italic().Bold()); + listing.GapLine(); + listing.End(); + _preset.DoContent(lGrid[1]); + + var vGrid = hGrid[1].GetVGrid(4f, 42f, 0f, 62f); + listing.Begin(vGrid[0]); + listing.Label(Lang.Get("Dialog_Rules.Configuration").Italic().Bold()); + listing.GapLine(); + listing.End(); + + var editMode = _preset.EditMode || (_template == _personalized); + + var color = GUI.color; + if (!editMode) { GUI.color = GuiPlus.ReadOnlyColor; } + + listing.Begin(vGrid[1]); + if (listing.ButtonText(Lang.Get("Rules.FoodRestrictions", _template.GetRestriction(RestrictionType.Food).Name.Bold()), Lang.Get("Rules.FoodRestrictionsDesc")) && editMode) { ChangeRestriction(RestrictionType.Food); } + if (_template.Type == PawnType.Colonist) + { + if (listing.ButtonText(Lang.Get("Rules.BondingRestrictions", _template.GetRestriction(RestrictionType.Bonding).Name.Bold()), Lang.Get("Rules.BondingRestrictionsDesc")) && editMode) { ChangeRestriction(RestrictionType.Bonding); } + listing.GapLine(); + listing.CheckboxLabeled(Lang.Get("Rules.AllowCourting"), ref _template.AllowCourting, Lang.Get("Rules.AllowCourtingDesc"), editMode); + listing.CheckboxLabeled(Lang.Get("Rules.AllowArtisan"), ref _template.AllowArtisan, Lang.Get("Rules.AllowArtisanDesc"), editMode); + } + listing.GapLine(); + listing.End(); + + if (_template.HasAddons) + { + var addonsRect = vGrid[1].GetVGrid(4f, listing.CurHeight, 0f)[1]; + _addons.Begin(addonsRect, addonsRect.height <= _template.AddonsRectHeight); + GuiPlus.DoAddonsListing(_addons, _template, editMode); + _addons.End(); + } + + GUI.color = color; + + var optionGrid = vGrid[2].GetVGrid(2f, 0f, 0f); + listing.Begin(optionGrid[0]); + if (listing.ButtonText(Lang.Get("Button.AssignTo"), Lang.Get("Button.AssignToDesc"), (_floatMenuAssign.Count > 0) && (!editMode || (_template == _personalized)))) { Find.WindowStack.Add(new FloatMenu(_floatMenuAssign)); } + listing.End(); + + listing.Begin(optionGrid[1]); + if (listing.ButtonText(_type == null ? Lang.Get("Button.ViewType", Lang.Get("PawnType.Individual")) : Lang.Get("Button.ViewTypeDefault", _template.Type.LabelPlural), Lang.Get("Button.ViewTypeDesc"), !editMode || (_template == _personalized))) { Find.WindowStack.Add(new FloatMenu(_floatMenuViews)); } + listing.End(); + + GUI.EndGroup(); + + if (GuiPlus.ButtonText(new Rect(rect.xMax - (80f - Margin), rect.yMax + (Margin * 2), OptionButtonSize, CloseButSize.y), Lang.Get("Button.GlobalOptions"), Lang.Get("Button.GlobalOptionsDesc"))) { Find.WindowStack.Add(new Dialog_Global()); } + GUI.BeginGroup(windowRect); + } + } +} diff --git a/Source/Interface/GuiPlus.cs b/Source/Interface/GuiPlus.cs new file mode 100644 index 0000000..c6cee19 --- /dev/null +++ b/Source/Interface/GuiPlus.cs @@ -0,0 +1,182 @@ +using System.Globalization; +using PawnRules.Data; +using PawnRules.Patch; +using RimWorld; +using UnityEngine; +using Verse; +using Verse.Sound; + +namespace PawnRules.Interface +{ + [StaticConstructorOnStartup] + internal static class GuiPlus + { + private const float CheckboxSize = 24f; + private const float ButtonSize = 30f; + private const float RadioButtonSize = 24f; + + private static readonly Color InactiveColor = new Color(0.37f, 0.37f, 0.37f, 0.8f); + public static readonly Color ReadOnlyColor = new Color(0.75f, 0.75f, 0.75f, 0.75f); + + private static readonly Texture2D EditRulesTexture = ContentFinder.Get("UI/EditRules"); + + public static Command_Action EditRulesCommand(Pawn pawn) => new Command_Action + { + icon = EditRulesTexture, + defaultLabel = Lang.Get("Gizmo.EditRulesLabel"), + defaultDesc = Lang.Get("Gizmo.EditRulesDesc", pawn.GetTargetType()?.Label.ToLower() ?? "pawn"), + action = () => Dialog_Rules.OpenFromPawn(pawn) + }; + + public static bool ButtonText(Rect rect, string label, string tooltip = null, bool enabled = true) + { + var result = Widgets.ButtonText(rect, label, true, false, enabled); + + if (Mouse.IsOver(rect) && enabled) + { + Widgets.DrawHighlight(rect); + if (!tooltip.NullOrEmpty()) { TooltipHandler.TipRegion(rect, tooltip); } + } + + if (!enabled) { Widgets.DrawBoxSolid(rect.ContractedBy(1f), InactiveColor); } + return result; + } + + public static bool RadioButtonInverted(Rect rect, string labelText, bool chosen, bool enabled) + { + var anchor = Text.Anchor; + Text.Anchor = TextAnchor.MiddleLeft; + var color = GUI.color; + if (!enabled) { GUI.color = InactiveColor; } + Widgets.Label(rect.AdjustedBy(RadioButtonSize + 4f, 0f, -(RadioButtonSize + 4f), 0f), labelText); + GUI.color = color; + Text.Anchor = anchor; + var labelClicked = Widgets.ButtonInvisible(rect); + var radioClicked = Widgets.RadioButton(rect.x, (rect.y + (rect.height / 2f)) - (RadioButtonSize / 2f), chosen); + if (labelClicked && !radioClicked && !chosen) { SoundDefOf.RadioButtonClicked.PlayOneShotOnCamera(); } + + return enabled && labelClicked; + } + + public static bool CheckboxPartial(Rect rect, string label, ref MultiCheckboxState state, string tooltip = null, bool enabled = true, bool allowPartialInCycle = true) + { + var prevAnchor = Text.Anchor; + Text.Anchor = TextAnchor.MiddleLeft; + Widgets.Label(rect, label); + + if (Mouse.IsOver(rect)) + { + Widgets.DrawHighlight(rect); + if (!tooltip.NullOrEmpty()) { TooltipHandler.TipRegion(rect, tooltip); } + } + + var result = false; + if (enabled && Widgets.ButtonInvisible(rect)) + { + if (state == MultiCheckboxState.Off) + { + state = allowPartialInCycle ? MultiCheckboxState.Partial : MultiCheckboxState.On; + SoundDefOf.Checkbox_TurnedOff.PlayOneShotOnCamera(); + } + else if (state == MultiCheckboxState.Partial) + { + state = MultiCheckboxState.On; + SoundDefOf.Checkbox_TurnedOn.PlayOneShotOnCamera(); + } + else + { + state = MultiCheckboxState.Off; + SoundDefOf.Checkbox_TurnedOff.PlayOneShotOnCamera(); + } + + result = true; + } + + var prevColor = GUI.color; + if (!enabled) { GUI.color = InactiveColor; } + + Texture2D image; + if (state == MultiCheckboxState.Partial) { image = Widgets.CheckboxPartialTex; } + else if (state == MultiCheckboxState.On) { image = Widgets.CheckboxOnTex; } + else { image = Widgets.CheckboxOffTex; } + + GUI.DrawTexture(new Rect((rect.x + rect.width) - CheckboxSize, rect.y, CheckboxSize, CheckboxSize), image); + if (!enabled) { GUI.color = prevColor; } + + Text.Anchor = prevAnchor; + + return result; + } + + public static string TextEntryLabeled(Rect rect, string label, string text, string tooltip = null) + { + if (Mouse.IsOver(rect)) + { + Widgets.DrawHighlight(rect); + if (!tooltip.NullOrEmpty()) { TooltipHandler.TipRegion(rect, tooltip); } + } + + var rect2 = rect.LeftHalf().Rounded(); + var rect3 = rect.RightHalf().Rounded(); + var anchor = Text.Anchor; + Text.Anchor = TextAnchor.MiddleLeft; + Widgets.Label(rect2, label); + Text.Anchor = anchor; + + return rect.height <= ButtonSize ? Widgets.TextField(rect3, text) : Widgets.TextArea(rect3, text); + } + + public static void DoAddonsListing(Listing_StandardPlus listing, Rules rules, bool editMode) + { + foreach (var addon in rules.GetAddons()) + { + if (rules.IsPreset && !addon.AllowedInPreset) { continue; } + + if (addon.Widget == OptionWidget.Checkbox) + { + var value = rules.GetAddonValue(addon, (bool) addon.DefaultValue); + if (!listing.CheckboxLabeled(addon.Label, ref value, addon.Tooltip, editMode)) { continue; } + if (rules.IsPreset) { addon.Handle.ChangePresetValue(rules, value); } + else { addon.Handle.ChangeValue(rules.Pawn, value); } + } + else if (addon.Widget == OptionWidget.TextEntry) + { + if (addon.Type == typeof(string)) + { + var oldValue = rules.GetAddonValue(addon, (string) addon.DefaultValue); + var newValue = listing.TextEntryLabeled(addon.Label, oldValue, addon.Tooltip); + if (!editMode || oldValue.Equals(newValue)) { continue; } + + if (rules.IsPreset) { addon.Handle.ChangePresetValue(rules, newValue); } + else { addon.Handle.ChangeValue(rules.Pawn, newValue); } + } + else if (addon.Type == typeof(int)) + { + var oldValue = rules.GetAddonValue(addon, (int) addon.DefaultValue).ToString(); + var newValue = listing.TextEntryLabeled(addon.Label, oldValue, addon.Tooltip); + if (!editMode || oldValue.Equals(newValue)) { continue; } + + if (rules.IsPreset) { addon.Handle.ChangePresetValue(rules, newValue.ToInt()); } + else { addon.Handle.ChangeValue(rules.Pawn, newValue.ToInt()); } + } + else if (addon.Type == typeof(float)) + { + var oldValue = rules.GetAddonValue(addon, (float) addon.DefaultValue).ToString(CultureInfo.InvariantCulture); + var newValue = listing.TextEntryLabeled(addon.Label, oldValue, addon.Tooltip); + if (!editMode || oldValue.Equals(newValue)) { continue; } + + if (rules.IsPreset) { addon.Handle.ChangePresetValue(rules, newValue.ToFloat()); } + else { addon.Handle.ChangeValue(rules.Pawn, newValue.ToFloat()); } + } + } + else if (addon.Widget == OptionWidget.Button) + { + if (!listing.ButtonText(addon.Label, addon.Tooltip) && editMode) { continue; } + + if (rules.IsPreset) { addon.Handle.DoDefaultClick(rules.Type.AsTarget); } + else { addon.Handle.DoClick(rules.Pawn); } + } + } + } + } +} diff --git a/Source/Interface/Listing_Preset.cs b/Source/Interface/Listing_Preset.cs new file mode 100644 index 0000000..0c0d261 --- /dev/null +++ b/Source/Interface/Listing_Preset.cs @@ -0,0 +1,146 @@ +using System; +using System.Linq; +using PawnRules.Data; +using PawnRules.Patch; +using UnityEngine; +using Verse; + +namespace PawnRules.Interface +{ + internal class Listing_Preset where T : Presetable + { + private readonly Listing_StandardPlus _presetListing = new Listing_StandardPlus(); + private readonly Listing_StandardPlus _listing = new Listing_StandardPlus(); + + public IPresetableType Type { get; set; } + + public Action OnSelect { get; } + public Action OnSave { get; } + public Action OnRevert { get; } + + public bool EditMode { get; set; } + + private T _lastSelected; + public T Selected { get; set; } + public T[] FixedPresets { get; set; } + public bool IsUnsaved => _lastSelected != null; + + public Listing_Preset(IPresetableType type, T selected, T[] fixedPresets, Action onSelect, Action onSave, Action onRevert) + { + Type = type; + Selected = selected; + FixedPresets = fixedPresets; + OnSelect = onSelect; + OnSave = onSave; + OnRevert = onRevert; + } + + private void ChangeEditMode(bool value) => EditMode = value; + + private void ChangeSelected(T selected) + { + if (Selected == selected) { return; } + Selected = selected; + OnSelect?.Invoke(); + } + + public void DoContent(Rect rect) + { + var selectedIsIgnored = Selected.IsIgnored(); + var presets = Registry.GetPresets(Type); + + _listing.Begin(rect); + foreach (var preset in FixedPresets) + { + var isSelected = (Selected == preset) || (Selected.Name == preset.Name); + if (_listing.RadioButtonInverted(preset.IsPreset ? preset.Name : Lang.Get("Preset.Personalized"), isSelected, null, !EditMode || isSelected)) { ChangeSelected(preset); } + } + if (presets.Any()) { _listing.GapLine(); } + _listing.End(); + + var presetGrid = rect.GetVGrid(4f, _listing.CurHeight, 0f, 62f); + + _presetListing.Begin(presetGrid[1], true); + foreach (var preset in presets) + { + var isSelected = (Selected == preset) || (Selected.Name == preset.Name); + if (_presetListing.RadioButtonInverted(preset.Name, isSelected, null, !EditMode || isSelected)) { ChangeSelected(preset); } + } + _presetListing.End(); + + var buttonGrid = presetGrid[2].GetHGrid(4f, 0f, 0f); + _listing.Begin(buttonGrid[0]); + + if (_listing.ButtonText(Lang.Get("Button.PresetNew"), Lang.Get("Button.PresetNewDesc"), !EditMode)) + { + Find.WindowStack.Add(new Dialog_PresetName(Type, preset => + { + _lastSelected = Selected; + ChangeEditMode(true); + ChangeSelected(preset); + })); + } + + if (EditMode) + { + if (_listing.ButtonText(Lang.Get("Button.PresetSave"), Lang.Get("Button.PresetSaveDesc"))) + { + Commit(); + ChangeEditMode(false); + OnSave?.Invoke(); + } + } + else if (Selected.IsPreset) + { + if (_listing.ButtonText(Lang.Get("Button.PresetEdit"), Lang.Get("Button.PresetEditDesc"), !Selected.IsVoid)) { ChangeEditMode(true); } + } + else + { + if (_listing.ButtonText(Lang.Get("Button.PresetSaveAs"), Lang.Get("Button.PresetSaveAsDesc"), !Selected.IsVoid && !selectedIsIgnored)) { OnSave?.Invoke(); } + } + + _listing.End(); + _listing.Begin(buttonGrid[1]); + + if (_listing.ButtonText(Lang.Get("Button.PresetDelete"), Lang.Get("Button.PresetDeleteDesc"), !Selected.IsVoid && Selected.IsPreset && !EditMode)) + { + Find.WindowStack.Add(new Dialog_Alert(Lang.Get("Button.PresetDeleteConfirm", Selected.Name), Dialog_Alert.Buttons.YesNo, () => + { + Registry.DeletePreset(Selected); + ChangeSelected(Registry.GetVoidPreset(Type)); + })); + } + + if (EditMode) + { + if (_listing.ButtonText(Lang.Get("Button.PresetRevert"), Lang.Get("Button.PresetRevertDesc"), !Selected.IsVoid)) + { + Revert(); + + ChangeEditMode(false); + OnRevert?.Invoke(); + } + } + else if (Selected.IsPreset) + { + if (_listing.ButtonText(Lang.Get("Button.PresetRename"), Lang.Get("Button.PresetRenameDesc"), !Selected.IsVoid && !EditMode)) { Find.WindowStack.Add(new Dialog_PresetName(Selected, ChangeSelected)); } + } + else + { + if (_listing.ButtonText(Lang.Get("Button.PresetClear"), Lang.Get("Button.PresetClearDesc"), !Selected.IsVoid && !selectedIsIgnored)) { OnRevert?.Invoke(); } + } + + _listing.End(); + } + + public void Revert() + { + if (!Selected.IsPreset || (_lastSelected == null)) { return; } + Registry.DeletePreset(Selected); + ChangeSelected(_lastSelected); + Commit(); + } + + public void Commit() => _lastSelected = null; + } +} diff --git a/Source/Interface/Listing_StandardPlus.cs b/Source/Interface/Listing_StandardPlus.cs new file mode 100644 index 0000000..4aadf7f --- /dev/null +++ b/Source/Interface/Listing_StandardPlus.cs @@ -0,0 +1,112 @@ +using UnityEngine; +using Verse; + +namespace PawnRules.Interface +{ + internal class Listing_StandardPlus : Listing_Standard + { + private const float ButtonHeight = 30f; + private const float ScrollbarSize = 20f; + private const float MaxScrollViewHeight = 999999f; + + private Vector2 _scrollPosition; + + private bool _scrollable; + + public override void Begin(Rect rect) { Begin(rect, false); } + public void Begin(Rect rect, bool scrollable) + { + _scrollable = scrollable; + var listRect = rect; + + if (_scrollable) + { + var viewRect = new Rect(0f, 0f, rect.width - ScrollbarSize, CurHeight); + + Widgets.BeginScrollView(rect, ref _scrollPosition, viewRect); + listRect = viewRect.AtZero(); + listRect.height = MaxScrollViewHeight; + } + + base.Begin(listRect); + } + + public override void End() + { + base.End(); + if (_scrollable) { Widgets.EndScrollView(); } + } + + public void NewColumn(float spacing, float width) + { + curY = 0f; + curX += ColumnWidth + spacing; + ColumnWidth = width; + } + + public void LabelMedium(string label) + { + var font = Text.Font; + Text.Font = GameFont.Medium; + Label(label); + Text.Font = font; + } + + public void LabelTiny(string label) + { + var font = Text.Font; + Text.Font = GameFont.Tiny; + Label(label); + Text.Font = font; + } + + public bool ButtonText(string label, string tooltip = null, bool enabled = true) + { + var result = GuiPlus.ButtonText(GetRect(ButtonHeight), label, tooltip, enabled); + Gap(verticalSpacing); + return result; + } + + public bool CheckboxLabeled(string label, ref bool checkOn, string tooltip = null, bool enabled = true) + { + var value = checkOn; + base.CheckboxLabeled(label, ref value, tooltip); + + if (!enabled) { return checkOn; } + + var result = checkOn != value; + checkOn = value; + return result; + } + + public bool CheckboxPartial(string label, ref MultiCheckboxState state, string tooltip = null, bool enabled = true, bool allowPartialInCycle = false) + { + var result = GuiPlus.CheckboxPartial(GetRect(Text.LineHeight), label, ref state, tooltip, enabled, allowPartialInCycle); + Gap(verticalSpacing); + + return result; + } + + public string TextEntryLabeled(string label, string text, string tooltip = null, int lineCount = 1) + { + var rect = GetRect(Text.LineHeight * lineCount); + var result = GuiPlus.TextEntryLabeled(rect, label, text, tooltip); + Gap(verticalSpacing); + return result; + } + + public bool RadioButtonInverted(string label, bool active, string tooltip = null, bool enabled = true) + { + var lineHeight = Text.LineHeight; + var rect = GetRect(lineHeight); + if (!tooltip.NullOrEmpty()) + { + if (Mouse.IsOver(rect)) { Widgets.DrawHighlight(rect); } + TooltipHandler.TipRegion(rect, tooltip); + } + var result = GuiPlus.RadioButtonInverted(rect, label, active, enabled); + Gap(verticalSpacing); + return result; + } + } +} diff --git a/Source/Interface/OptionWidget.cs b/Source/Interface/OptionWidget.cs new file mode 100644 index 0000000..6b74f27 --- /dev/null +++ b/Source/Interface/OptionWidget.cs @@ -0,0 +1,9 @@ +namespace PawnRules.Interface +{ + internal enum OptionWidget + { + Button, + Checkbox, + TextEntry + } +} diff --git a/Source/Interface/WindowPlus.cs b/Source/Interface/WindowPlus.cs new file mode 100644 index 0000000..0747649 --- /dev/null +++ b/Source/Interface/WindowPlus.cs @@ -0,0 +1,60 @@ +using UnityEngine; +using Verse; + +namespace PawnRules.Interface +{ + internal abstract class WindowPlus : Window + { + public override Vector2 InitialSize { get; } + public string Title { get; set; } + + protected WindowPlus(Vector2 size) : this(null, size) + { } + + protected WindowPlus(string title = null, Vector2 size = default(Vector2)) + { + draggable = true; + doCloseX = true; + doCloseButton = true; + absorbInputAroundWindow = true; + closeOnClickedOutside = false; + closeOnAccept = false; + + InitialSize = size == default(Vector2) ? new Vector2(800f, 600f) : size; + Title = title; + } + + public abstract void DoContent(Rect rect); + + public override void DoWindowContents(Rect rect) + { + var wordWrap = Text.WordWrap; + Text.WordWrap = false; + + DoContent(DoTitle(rect)); + + Text.WordWrap = wordWrap; + } + + private Rect DoTitle(Rect rect) + { + if (Title.NullOrEmpty()) { return rect; } + + var header = new Listing_StandardPlus(); + + header.Begin(rect); + Text.Font = GameFont.Medium; + header.LabelMedium(Title); + header.GapLine(); + header.End(); + + var contentRect = rect; + contentRect.y += header.CurHeight; + contentRect.height -= header.CurHeight; + + if (doCloseButton) { contentRect.height -= 55f; } + + return contentRect; + } + } +} diff --git a/Source/Mod.cs b/Source/Mod.cs new file mode 100644 index 0000000..efce907 --- /dev/null +++ b/Source/Mod.cs @@ -0,0 +1,24 @@ +using Verse; + +namespace PawnRules +{ + internal class Mod : Verse.Mod + { + public const string Id = "PawnRules"; + public const string Name = "Pawn Rules"; + public const string Author = "Jaxe"; + public const string Version = "1.0"; + + public static ModContentPack ContentPack { get; private set; } + + public Mod(ModContentPack contentPack) : base(contentPack) => ContentPack = contentPack; + + public static void Log(string message) => Verse.Log.Message($"[{Name}] {message}"); + + internal class Exception : System.Exception + { + public Exception(string message) : base($"[{Name} : EXCEPTION] {message}") + { } + } + } +} diff --git a/Source/Patch/Extensions.cs b/Source/Patch/Extensions.cs new file mode 100644 index 0000000..18f0871 --- /dev/null +++ b/Source/Patch/Extensions.cs @@ -0,0 +1,145 @@ +using System; +using PawnRules.Data; +using RimWorld; +using UnityEngine; +using Verse; + +namespace PawnRules.Patch +{ + internal static class Extensions + { + public static string Italic(this string self) => "" + self + ""; + public static string Bold(this string self) => "" + self + ""; + + public static string GetCategoryLabel(this ThingDef self) => self.category == ThingCategory.Item ? self.FirstThingCategory.LabelCap : self.category.ToString(); + public static Rect AdjustedBy(this Rect self, float x, float y, float width, float height) => new Rect(self.x + x, self.y + y, self.width + width, self.height + height); + + public static int LastIndex(this Array self) => self.Length - 1; + + public static int ToInt(this string self, int defaultValue = 0) => int.TryParse(self, out var result) ? result : defaultValue; + public static float ToFloat(this string self, float defaultValue = 0f) => float.TryParse(self, out var result) ? result : defaultValue; + + public static bool CanHaveRules(this Pawn self) => (self != null) && !self.Dead && (self.GetTargetType() != null); + + public static PawnType GetTargetType(this Pawn self) + { + if (self == null) { return null; } + if ((self.Faction == Faction.OfPlayer) && self.IsColonist) { return PawnType.Colonist; } + if ((self.Faction == Faction.OfPlayer) && self.RaceProps.Animal) { return PawnType.Animal; } + if (self.HostFaction == Faction.OfPlayer) { return self.IsPrisonerOfColony ? PawnType.Prisoner : PawnType.Guest; } + return null; + } + + public static Rect[] GetHGrid(this Rect self, float spacing, params float[] widths) + { + var unfixedCount = 0; + var currentX = self.x; + var fixedWidths = 0f; + var rects = new Rect[widths.Length]; + + for (var index = 0; index < widths.Length; index++) + { + var width = widths[index]; + if (width > 0) { fixedWidths += width; } + else { unfixedCount++; } + + if (index != widths.LastIndex()) { fixedWidths += spacing; } + } + + var unfixedWidth = unfixedCount > 0 ? (self.width - fixedWidths) / unfixedCount : 0; + + for (var index = 0; index < widths.Length; index++) + { + var width = widths[index]; + float newWidth; + if (width > 0) + { + newWidth = width; + rects[index] = new Rect(currentX, self.y, newWidth, self.height); + } + else + { + newWidth = unfixedWidth; + rects[index] = new Rect(currentX, self.y, newWidth, self.height); + } + currentX += newWidth + spacing; + } + + return rects; + } + + public static Rect[] GetVGrid(this Rect self, float spacing, params float[] heights) + { + var unfixedCount = 0; + var currentY = self.y; + var fixedHeights = 0f; + var rects = new Rect[heights.Length]; + + for (var index = 0; index < heights.Length; index++) + { + var height = heights[index]; + if (height > 0) { fixedHeights += height; } + else { unfixedCount++; } + + if (index != heights.LastIndex()) { fixedHeights += spacing; } + } + + var unfixedWidth = unfixedCount > 0 ? (self.height - fixedHeights) / unfixedCount : 0; + + for (var index = 0; index < heights.Length; index++) + { + var height = heights[index]; + float newHeight; + if (height > 0) + { + newHeight = height; + rects[index] = new Rect(self.x, currentY, self.width, newHeight); + } + else + { + newHeight = unfixedWidth; + rects[index] = new Rect(self.x, currentY, self.width, newHeight); + } + currentY += newHeight + spacing; + } + + return rects; + } +/* + public static Rect[] GetVGrid(this Rect self, float spacing, params float[] heights) + { + var unfixedCount = 0; + var currentY = self.y; + var fixedHeights = 0f; + var rects = new Rect[heights.Length]; + + foreach (var height in heights) + { + if (height > 0) { fixedHeights += height - (spacing * 2); } + else { unfixedCount++; } + } + + var unfixedHeight = unfixedCount > 0 ? (self.height - fixedHeights) / unfixedCount : 0; + + for (var index = 0; index < heights.Length; index++) + { + var height = heights[index]; + float newHeight; + if (height > 0) + { + newHeight = height; + rects[index] = new Rect(self.x, currentY, self.width, newHeight); + } + else + { + newHeight = unfixedHeight; + rects[index] = new Rect(self.x, currentY, self.width, newHeight); + } + currentY += newHeight + spacing; + } + + return rects; + } + */ + } +} diff --git a/Source/Patch/PrivateAccess.cs b/Source/Patch/PrivateAccess.cs new file mode 100644 index 0000000..835d0a9 --- /dev/null +++ b/Source/Patch/PrivateAccess.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Harmony; +using RimWorld; +using Verse; +using Verse.AI; + +namespace PawnRules.Patch +{ + internal static class PrivateAccess + { + private static readonly MethodInfo Method_RimWorld_FoodUtility_SpawnedFoodSearchInnerScan = AccessTools.Method(typeof(FoodUtility), "SpawnedFoodSearchInnerScan"); + private static readonly MethodInfo Method_RimWorld_FoodUtility_GetMaxRegionsToScan = AccessTools.Method(typeof(FoodUtility), "GetMaxRegionsToScan"); + private static readonly MethodInfo Method_RimWorld_FoodUtility_IsFoodSourceOnMapSociallyProper = AccessTools.Method(typeof(FoodUtility), "IsFoodSourceOnMapSociallyProper"); + private static readonly FieldInfo Field_RimWorld_FoodUtility_Filtered = AccessTools.Field(typeof(FoodUtility), "filtered"); + private static readonly FieldInfo Field_Verse_LoadedModManager_RunningMods = AccessTools.Field(typeof(LoadedModManager), "runningMods"); + + public static bool RimWorld_FoodUtility_IsFoodSourceOnMapSociallyProper(Thing thing, Pawn getter, Pawn eater, bool allowSociallyImproper) => (bool) Method_RimWorld_FoodUtility_IsFoodSourceOnMapSociallyProper.Invoke(null, new object[] { thing, getter, eater, allowSociallyImproper }); + public static Thing RimWorld_FoodUtility_SpawnedFoodSearchInnerScan(Pawn eater, IntVec3 root, List searchSet, PathEndMode peMode, TraverseParms traverseParams, float maxDistance = 9999f, Predicate validator = null) => (Thing) Method_RimWorld_FoodUtility_SpawnedFoodSearchInnerScan.Invoke(null, new object[] { eater, root, searchSet, peMode, traverseParams, maxDistance, validator }); + public static int RimWorld_FoodUtility_GetMaxRegionsToScan(Pawn getter, bool forceScanWholeMap) => (int) Method_RimWorld_FoodUtility_GetMaxRegionsToScan.Invoke(null, new object[] { getter, forceScanWholeMap }); + public static HashSet RimWorld_FoodUtility_Filtered() => (HashSet) Field_RimWorld_FoodUtility_Filtered.GetValue(null); + public static List Verse_LoadedModManager_RunningMods() => (List) Field_Verse_LoadedModManager_RunningMods.GetValue(null); + } +} diff --git a/Source/Patch/RimWorld_FoodUtility_BestFoodInInventory.cs b/Source/Patch/RimWorld_FoodUtility_BestFoodInInventory.cs new file mode 100644 index 0000000..4d798ac --- /dev/null +++ b/Source/Patch/RimWorld_FoodUtility_BestFoodInInventory.cs @@ -0,0 +1,36 @@ +using Harmony; +using PawnRules.Data; +using RimWorld; +using Verse; + +namespace PawnRules.Patch +{ + [HarmonyPatch(typeof(FoodUtility), nameof(FoodUtility.BestFoodInInventory))] + internal static class RimWorld_FoodUtility_BestFoodInInventory + { + private static bool Prefix(ref Thing __result, Pawn holder, Pawn eater = null, FoodPreferability minFoodPref = FoodPreferability.NeverForNutrition, FoodPreferability maxFoodPref = FoodPreferability.MealLavish, float minStackNutrition = 0.0f, bool allowDrug = false) + { + if (!Registry.IsActive) { return true; } + + if (eater == null) { eater = holder; } + var rules = Registry.GetRules(eater); + if (eater.InMentalState || (rules == null) || rules.GetRestriction(RestrictionType.Food).IsVoid) { return true; } + + if (holder.inventory == null) + { + __result = null; + return false; + } + + var innerContainer = holder.inventory.innerContainer; + foreach (var thing in innerContainer) + { + if (rules.GetRestriction(RestrictionType.Food).Allows(thing.def) && thing.def.IsNutritionGivingIngestible && thing.IngestibleNow && eater.RaceProps.CanEverEat(thing) && (thing.def.ingestible.preferability >= minFoodPref) && (thing.def.ingestible.preferability <= maxFoodPref) && (allowDrug || !thing.def.IsDrug) && (thing.GetStatValue(StatDefOf.Nutrition) * thing.stackCount >= (double) minStackNutrition)) { __result = thing; } + return false; + } + + __result = null; + return false; + } + } +} diff --git a/Source/Patch/RimWorld_FoodUtility_BestFoodSourceOnMap.cs b/Source/Patch/RimWorld_FoodUtility_BestFoodSourceOnMap.cs new file mode 100644 index 0000000..bdac04c --- /dev/null +++ b/Source/Patch/RimWorld_FoodUtility_BestFoodSourceOnMap.cs @@ -0,0 +1,137 @@ +using System; +using Harmony; +using PawnRules.Data; +using RimWorld; +using UnityEngine.Profiling; +using Verse; +using Verse.AI; + +namespace PawnRules.Patch +{ + [HarmonyPatch(typeof(FoodUtility), nameof(FoodUtility.BestFoodSourceOnMap))] + internal static class RimWorld_FoodUtility_BestFoodSourceOnMap + { + private static bool Prefix(ref Thing __result, Pawn getter, Pawn eater, bool desperate, out ThingDef foodDef, FoodPreferability maxPref = FoodPreferability.MealLavish, bool allowPlant = true, bool allowDrug = true, bool allowCorpse = true, bool allowDispenserFull = true, bool allowDispenserEmpty = true, bool allowForbidden = false, bool allowSociallyImproper = false, bool allowHarvest = false, bool forceScanWholeMap = false) + { + foodDef = null; + + if (!Registry.IsActive) { return true; } + + var rules = Registry.GetRules(eater); + if (eater.InMentalState || (rules == null) || (rules.GetRestriction(RestrictionType.Food).IsVoid)) { return true; } + + Profiler.BeginSample("BestFoodInWorldFor getter=" + getter.LabelCap + " eater=" + eater.LabelCap); + var getterCanManipulate = getter.RaceProps.ToolUser && getter.health.capacities.CapableOf(PawnCapacityDefOf.Manipulation); + if (!getterCanManipulate && (getter != eater)) + { + Log.Error(getter + " tried to find food to bring to " + eater + " but " + getter + " is incapable of Manipulation."); + Profiler.EndSample(); + + __result = null; + return false; + } + + var minPref = !eater.NonHumanlikeOrWildMan() ? (!desperate ? (eater.needs.food.CurCategory < HungerCategory.UrgentlyHungry ? FoodPreferability.MealAwful : FoodPreferability.RawBad) : FoodPreferability.DesperateOnly) : FoodPreferability.NeverForNutrition; + var foodValidator = (Predicate) (thing => + { + Profiler.BeginSample("foodValidator"); + if (!rules.GetRestriction(RestrictionType.Food).Allows(thing.def)) + { + Profiler.EndSample(); + return false; + } + if (thing is Building_NutrientPasteDispenser nutrientPasteDispenser) + { + if (!allowDispenserFull || !getterCanManipulate || (ThingDefOf.MealNutrientPaste.ingestible.preferability < minPref) || (ThingDefOf.MealNutrientPaste.ingestible.preferability > maxPref) || !eater.RaceProps.CanEverEat(ThingDefOf.MealNutrientPaste) || ((thing.Faction != getter.Faction) && (thing.Faction != getter.HostFaction)) || (!allowForbidden && thing.IsForbidden(getter)) || !nutrientPasteDispenser.powerComp.PowerOn || (!allowDispenserEmpty && !nutrientPasteDispenser.HasEnoughFeedstockInHoppers()) || !thing.InteractionCell.Standable(thing.Map) || !PrivateAccess.RimWorld_FoodUtility_IsFoodSourceOnMapSociallyProper(thing, getter, eater, allowSociallyImproper) || getter.IsWildMan() || !getter.Map.reachability.CanReachNonLocal(getter.Position, new TargetInfo(thing.InteractionCell, thing.Map), PathEndMode.OnCell, TraverseParms.For(getter, Danger.Some))) + { + Profiler.EndSample(); + return false; + } + } + else if ((thing.def.ingestible.preferability < minPref) || (thing.def.ingestible.preferability > maxPref) || !eater.RaceProps.WillAutomaticallyEat(thing) || !thing.def.IsNutritionGivingIngestible || !thing.IngestibleNow || (!allowCorpse && thing is Corpse) || (!allowDrug && thing.def.IsDrug) || (!allowForbidden && thing.IsForbidden(getter)) || (!desperate && thing.IsNotFresh()) || thing.IsDessicated() || !PrivateAccess.RimWorld_FoodUtility_IsFoodSourceOnMapSociallyProper(thing, getter, eater, allowSociallyImproper) || (!getter.AnimalAwareOf(thing) && !forceScanWholeMap) || !getter.CanReserve((LocalTargetInfo) thing)) + { + Profiler.EndSample(); + return false; + } + + Profiler.EndSample(); + return true; + }); + + var req = ((eater.RaceProps.foodType & (FoodTypeFlags.Plant | FoodTypeFlags.Tree)) == FoodTypeFlags.None) || !allowPlant ? ThingRequest.ForGroup(ThingRequestGroup.FoodSourceNotPlantOrTree) : ThingRequest.ForGroup(ThingRequestGroup.FoodSource); + Thing bestThing; + + if (getter.RaceProps.Humanlike) + { + bestThing = PrivateAccess.RimWorld_FoodUtility_SpawnedFoodSearchInnerScan(eater, getter.Position, getter.Map.listerThings.ThingsMatching(req), PathEndMode.ClosestTouch, TraverseParms.For(getter), 9999f, foodValidator); + + if (allowHarvest && getterCanManipulate) + { + var searchRegionsMax = !forceScanWholeMap || (bestThing != null) ? 30 : -1; + + bool Validator(Thing thing) + { + if (!rules.GetRestriction(RestrictionType.Food).Allows(thing.def)) { return false; } + + var plant = (Plant) thing; + if (!plant.HarvestableNow) { return false; } + + var harvestedThingDef = plant.def.plant.harvestedThingDef; + return harvestedThingDef.IsNutritionGivingIngestible && eater.RaceProps.CanEverEat(harvestedThingDef) && getter.CanReserve((LocalTargetInfo) plant) && (allowForbidden || !plant.IsForbidden(getter)) && ((bestThing == null) || (FoodUtility.GetFinalIngestibleDef(bestThing).ingestible.preferability < harvestedThingDef.ingestible.preferability)); + } + + var foodSource = GenClosest.ClosestThingReachable(getter.Position, getter.Map, ThingRequest.ForGroup(ThingRequestGroup.HarvestablePlant), PathEndMode.Touch, TraverseParms.For(getter), 9999f, Validator, null, 0, searchRegionsMax); + + if (foodSource != null) + { + bestThing = foodSource; + foodDef = FoodUtility.GetFinalIngestibleDef(foodSource, true); + } + } + + if ((foodDef == null) && (bestThing != null)) { foodDef = FoodUtility.GetFinalIngestibleDef(bestThing); } + } + else + { + var maxRegionsToScan = PrivateAccess.RimWorld_FoodUtility_GetMaxRegionsToScan(getter, forceScanWholeMap); + PrivateAccess.RimWorld_FoodUtility_Filtered().Clear(); + + foreach (var thing in GenRadial.RadialDistinctThingsAround(getter.Position, getter.Map, 2f, true)) + { + if (thing is Pawn pawn && (pawn != getter) && pawn.RaceProps.Animal && (pawn.CurJob != null) && (pawn.CurJob.def == JobDefOf.Ingest) && pawn.CurJob.GetTarget(TargetIndex.A).HasThing) { PrivateAccess.RimWorld_FoodUtility_Filtered().Add(pawn.CurJob.GetTarget(TargetIndex.A).Thing); } + } + + var flag = !allowForbidden && ForbidUtility.CaresAboutForbidden(getter, true) && (getter.playerSettings?.EffectiveAreaRestrictionInPawnCurrentMap != null); + var predicate = (Predicate) (thing => foodValidator(thing) && !PrivateAccess.RimWorld_FoodUtility_Filtered().Contains(thing) && (thing is Building_NutrientPasteDispenser || (thing.def.ingestible.preferability > FoodPreferability.DesperateOnly)) && !thing.IsNotFresh()); + var position1 = getter.Position; + var map1 = getter.Map; + var thingReq1 = req; + var traverseParams1 = TraverseParms.For(getter); + var validator1 = predicate; + var ignoreEntirelyForbiddenRegions1 = flag; + + bestThing = GenClosest.ClosestThingReachable(position1, map1, thingReq1, PathEndMode.ClosestTouch, traverseParams1, 9999f, validator1, null, 0, maxRegionsToScan, false, RegionType.Set_Passable, ignoreEntirelyForbiddenRegions1); + + PrivateAccess.RimWorld_FoodUtility_Filtered().Clear(); + + if (bestThing == null) + { + desperate = true; + var position2 = getter.Position; + var map2 = getter.Map; + var thingReq2 = req; + var traverseParams2 = TraverseParms.For(getter); + var validator2 = foodValidator; + var ignoreEntirelyForbiddenRegions2 = flag; + bestThing = GenClosest.ClosestThingReachable(position2, map2, thingReq2, PathEndMode.ClosestTouch, traverseParams2, 9999f, validator2, null, 0, maxRegionsToScan, false, RegionType.Set_Passable, ignoreEntirelyForbiddenRegions2); + } + + if (bestThing != null) { foodDef = FoodUtility.GetFinalIngestibleDef(bestThing); } + } + + Profiler.EndSample(); + __result = bestThing; + return false; + } + } +} diff --git a/Source/Patch/RimWorld_GenConstruct_CanConstruct.cs b/Source/Patch/RimWorld_GenConstruct_CanConstruct.cs new file mode 100644 index 0000000..afd9715 --- /dev/null +++ b/Source/Patch/RimWorld_GenConstruct_CanConstruct.cs @@ -0,0 +1,26 @@ +using Harmony; +using PawnRules.Data; +using RimWorld; +using Verse; +using Verse.AI; + +namespace PawnRules.Patch +{ + [HarmonyPatch(typeof(GenConstruct), nameof(GenConstruct.CanConstruct))] + internal static class RimWorld_GenConstruct_CanConstruct + { + private static void Postfix(ref bool __result, Thing t, Pawn p, bool checkConstructionSkill = true, bool forced = false) + { + if (!Registry.IsActive) { return; } + + var rules = Registry.GetRules(p); + if (!checkConstructionSkill || (rules == null) || rules.AllowArtisan || !((ThingDef) t.def.entityDefToBuild).HasComp(typeof(CompQuality))) + { + if (forced && !JobFailReason.HaveReason) { JobFailReason.Is(Lang.Get("Rules.NotArtisanReason"), Lang.Get("Rules.NotArtisanJob", t.LabelCap)); } + return; + } + + __result = false; + } + } +} diff --git a/Source/Patch/RimWorld_InteractionWorker_RomanceAttempt_SuccessChance.cs b/Source/Patch/RimWorld_InteractionWorker_RomanceAttempt_SuccessChance.cs new file mode 100644 index 0000000..27f2552 --- /dev/null +++ b/Source/Patch/RimWorld_InteractionWorker_RomanceAttempt_SuccessChance.cs @@ -0,0 +1,24 @@ +using Harmony; +using PawnRules.Data; +using RimWorld; +using Verse; + +namespace PawnRules.Patch +{ + [HarmonyPatch(typeof(InteractionWorker_RomanceAttempt), nameof(InteractionWorker_RomanceAttempt.SuccessChance))] + internal static class RimWorld_InteractionWorker_RomanceAttempt_SuccessChance + { + private static bool Prefix(ref float __result, Pawn initiator, Pawn recipient) + { + if (!Registry.IsActive) { return true; } + + var initiatorCanCourt = Registry.GetRules(initiator)?.AllowCourting ?? true; + var recipientCanCourt = Registry.GetRules(recipient)?.AllowCourting ?? true; + + if (initiatorCanCourt && recipientCanCourt) { return true; } + + __result = 0f; + return false; + } + } +} diff --git a/Source/Patch/RimWorld_JobGiver_PackFood.cs b/Source/Patch/RimWorld_JobGiver_PackFood.cs new file mode 100644 index 0000000..495aaaf --- /dev/null +++ b/Source/Patch/RimWorld_JobGiver_PackFood.cs @@ -0,0 +1,21 @@ +using Harmony; +using PawnRules.Data; +using RimWorld; +using Verse; + +namespace PawnRules.Patch +{ + [HarmonyPatch(typeof(JobGiver_PackFood), "IsGoodPackableFoodFor")] + internal static class RimWorld_JobGiver_PackFood + { + private static void Postfix(ref bool __result, Thing food, Pawn forPawn) + { + if (!Registry.IsActive) { return; } + + var rules = Registry.GetRules(forPawn); + if (forPawn.InMentalState || (rules == null) || rules.GetRestriction(RestrictionType.Food).IsVoid) { return; } + + __result = __result && rules.GetRestriction(RestrictionType.Food).Allows(food.def); + } + } +} diff --git a/Source/Patch/RimWorld_JoyGiver_Ingest.cs b/Source/Patch/RimWorld_JoyGiver_Ingest.cs new file mode 100644 index 0000000..fbd156a --- /dev/null +++ b/Source/Patch/RimWorld_JoyGiver_Ingest.cs @@ -0,0 +1,21 @@ +using Harmony; +using PawnRules.Data; +using RimWorld; +using Verse; + +namespace PawnRules.Patch +{ + [HarmonyPatch(typeof(JoyGiver_Ingest), "CanIngestForJoy")] + internal static class RimWorld_JoyGiver_Ingest + { + private static bool Prefix(ref bool __result, Pawn pawn, Thing t) + { + var rules = Registry.GetRules(pawn); + var restriction = rules?.GetRestriction(RestrictionType.Food); + if (pawn.InMentalState || (restriction == null) || restriction.IsVoid || restriction.Allows(t.def)) { return true; } + + __result = false; + return false; + } + } +} diff --git a/Source/Patch/RimWorld_PawnUtility_TrySpawnHatchedOrBornPawn.cs b/Source/Patch/RimWorld_PawnUtility_TrySpawnHatchedOrBornPawn.cs new file mode 100644 index 0000000..4b017e0 --- /dev/null +++ b/Source/Patch/RimWorld_PawnUtility_TrySpawnHatchedOrBornPawn.cs @@ -0,0 +1,17 @@ +using Harmony; +using PawnRules.Data; +using RimWorld; +using Verse; + +namespace PawnRules.Patch +{ + [HarmonyPatch(typeof(PawnUtility), nameof(PawnUtility.TrySpawnHatchedOrBornPawn))] + internal static class RimWorld_PawnUtility_TrySpawnHatchedOrBornPawn + { + private static void Postfix(bool __result, Pawn pawn, Thing motherOrEgg) + { + if (!Registry.IsActive || !__result || (pawn == null) || !(motherOrEgg is Pawn mother) || (mother == null) || (mother.Faction != Faction.OfPlayer)) { return; } + Registry.CloneRules(mother, pawn); + } + } +} diff --git a/Source/Patch/RimWorld_RelationsUtility_TryDevelopBondRelation.cs b/Source/Patch/RimWorld_RelationsUtility_TryDevelopBondRelation.cs new file mode 100644 index 0000000..5fbc14b --- /dev/null +++ b/Source/Patch/RimWorld_RelationsUtility_TryDevelopBondRelation.cs @@ -0,0 +1,20 @@ +using Harmony; +using PawnRules.Data; +using RimWorld; +using Verse; + +namespace PawnRules.Patch +{ + [HarmonyPatch(typeof(RelationsUtility), nameof(RelationsUtility.TryDevelopBondRelation))] + internal static class RimWorld_RelationsUtility_TryDevelopBondRelation + { + private static bool Prefix(Pawn humanlike, Pawn animal) + { + if (!Registry.IsActive || !humanlike.CanHaveRules()) { return true; } + + var rules = Registry.GetRules(humanlike); + var restrictions = rules.GetRestriction(RestrictionType.Bonding); + return (rules == null) || (restrictions.IsVoid) || restrictions.Allows(animal.def); + } + } +} diff --git a/Source/Patch/Verse_Game_FinalizeInit.cs b/Source/Patch/Verse_Game_FinalizeInit.cs new file mode 100644 index 0000000..b992d5c --- /dev/null +++ b/Source/Patch/Verse_Game_FinalizeInit.cs @@ -0,0 +1,11 @@ +using Harmony; +using Verse; + +namespace PawnRules.Patch +{ + [HarmonyPatch(typeof(Game), nameof(Game.FinalizeInit))] + internal static class Verse_Game_FinalizeInit + { + private static void Postfix() => Controller.LoadWorld(); + } +} diff --git a/Source/Patch/Verse_Pawn_GetGizmos.cs b/Source/Patch/Verse_Pawn_GetGizmos.cs new file mode 100644 index 0000000..048e580 --- /dev/null +++ b/Source/Patch/Verse_Pawn_GetGizmos.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using Harmony; +using PawnRules.Data; +using PawnRules.Interface; +using Verse; + +namespace PawnRules.Patch +{ + [HarmonyPatch(typeof(Pawn), nameof(Pawn.GetGizmos))] + internal static class Verse_Pawn_GetGizmos + { + private static void Postfix(Pawn __instance, ref IEnumerable __result) + { + if (!Registry.IsActive || (Find.Selector.NumSelected != 1) || (__instance == null) || !__instance.CanHaveRules()) { return; } + __result = new List(__result) { GuiPlus.EditRulesCommand(__instance) }; + } + } +} diff --git a/Source/Patch/Verse_Pawn_GuestTracker_SetGuestStatus.cs b/Source/Patch/Verse_Pawn_GuestTracker_SetGuestStatus.cs new file mode 100644 index 0000000..6ab6a8b --- /dev/null +++ b/Source/Patch/Verse_Pawn_GuestTracker_SetGuestStatus.cs @@ -0,0 +1,17 @@ +using Harmony; +using PawnRules.Data; +using RimWorld; +using Verse; + +namespace PawnRules.Patch +{ + [HarmonyPatch(typeof(Pawn_GuestTracker), nameof(Pawn_GuestTracker.SetGuestStatus))] + internal static class Verse_Pawn_GuestTracker_SetGuestStatus + { + private static void Prefix(Pawn_GuestTracker __instance, Faction newHost, bool prisoner = false) + { + if (!Registry.IsActive) { return; } + Registry.FactionUpdate(Traverse.Create(__instance).Field("pawn").GetValue(), newHost, !prisoner); + } + } +} diff --git a/Source/Patch/Verse_Pawn_Kill.cs b/Source/Patch/Verse_Pawn_Kill.cs new file mode 100644 index 0000000..6edb494 --- /dev/null +++ b/Source/Patch/Verse_Pawn_Kill.cs @@ -0,0 +1,16 @@ +using Harmony; +using PawnRules.Data; +using Verse; + +namespace PawnRules.Patch +{ + [HarmonyPatch(typeof(Pawn), nameof(Pawn.Kill))] + internal static class Verse_Pawn_Kill + { + private static void Postfix(Pawn __instance) + { + if (!Registry.IsActive) { return; } + Registry.DeleteRules(__instance); + } + } +} diff --git a/Source/Patch/Verse_Pawn_SetFaction.cs b/Source/Patch/Verse_Pawn_SetFaction.cs new file mode 100644 index 0000000..52a84b3 --- /dev/null +++ b/Source/Patch/Verse_Pawn_SetFaction.cs @@ -0,0 +1,17 @@ +using Harmony; +using PawnRules.Data; +using RimWorld; +using Verse; + +namespace PawnRules.Patch +{ + [HarmonyPatch(typeof(Pawn), nameof(Pawn.SetFaction))] + internal static class Verse_Pawn_SetFaction + { + private static void Prefix(Pawn __instance, Faction newFaction) + { + if (!Registry.IsActive) { return; } + Registry.FactionUpdate(__instance, newFaction); + } + } +} diff --git a/Source/Patch/Verse_Pawn_SetFactionDirect.cs b/Source/Patch/Verse_Pawn_SetFactionDirect.cs new file mode 100644 index 0000000..a6338e6 --- /dev/null +++ b/Source/Patch/Verse_Pawn_SetFactionDirect.cs @@ -0,0 +1,17 @@ +using Harmony; +using PawnRules.Data; +using RimWorld; +using Verse; + +namespace PawnRules.Patch +{ + [HarmonyPatch(typeof(Pawn), nameof(Pawn.SetFactionDirect))] + internal static class Verse_Pawn_SetFactionDirect + { + private static void Prefix(Pawn __instance, Faction newFaction) + { + if (!Registry.IsActive) { return; } + Registry.FactionUpdate(__instance, newFaction); + } + } +} diff --git a/Source/PawnRules.csproj b/Source/PawnRules.csproj new file mode 100644 index 0000000..d4d8644 --- /dev/null +++ b/Source/PawnRules.csproj @@ -0,0 +1,131 @@ + + + + + Debug + AnyCPU + {A4DC7884-6BF7-4787-A274-ABF85D361DE0} + Library + Properties + PawnRules + PawnRules + v3.5 + 512 + + + + false + none + false + ..\Assemblies\ + DEBUG;TRACE + prompt + 4 + false + default + ..\Assemblies\PawnRules.xml + + + none + true + ..\Assemblies\ + TRACE + prompt + 4 + false + ..\Assemblies\PawnRules.xml + + + + False + ..\..\..\..\..\Dev\RimDev\Harmony\New\0Harmony.dll + + + ..\..\..\RimWorldWin64_Data\Managed\Assembly-CSharp.dll + False + + + + + + ..\..\..\RimWorldWin64_Data\Managed\UnityEngine.dll + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -\README.md + + + + + -\.gitignore + + + -\About\About.xml + + + -\About\ModSync.xml + + + -\Defs\WorldObjectDefs\PawnRules.xml + + + -\Languages\English\Keyed\PawnRules.xml + Designer + + + -\Textures\UI\EditRules.png + + + + + \ No newline at end of file diff --git a/Source/PawnRules.sln b/Source/PawnRules.sln new file mode 100644 index 0000000..27858a2 --- /dev/null +++ b/Source/PawnRules.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26228.9 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PawnRules", "PawnRules.csproj", "{A4DC7884-6BF7-4787-A274-ABF85D361DE0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A4DC7884-6BF7-4787-A274-ABF85D361DE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A4DC7884-6BF7-4787-A274-ABF85D361DE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A4DC7884-6BF7-4787-A274-ABF85D361DE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A4DC7884-6BF7-4787-A274-ABF85D361DE0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/Source/Properties/AssemblyInfo.cs b/Source/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..a3ce1d7 --- /dev/null +++ b/Source/Properties/AssemblyInfo.cs @@ -0,0 +1,9 @@ +using System.Reflection; +using PawnRules; + +[assembly: AssemblyTitle(Mod.Id)] +[assembly: AssemblyCompany(Mod.Author)] +[assembly: AssemblyProduct(Mod.Id)] +[assembly: AssemblyCopyright("© " + Mod.Author)] +[assembly: AssemblyVersion(Mod.Version)] +[assembly: AssemblyFileVersion(Mod.Version)] diff --git a/Source/SDK/OptionHandle.cs b/Source/SDK/OptionHandle.cs new file mode 100644 index 0000000..28fb01e --- /dev/null +++ b/Source/SDK/OptionHandle.cs @@ -0,0 +1,144 @@ +using PawnRules.Data; +using Verse; + +namespace PawnRules.SDK +{ + /// + /// The base class of a rules option handle. + /// + public abstract class OptionHandle + { + /// + /// Called when the button for this option is clicked. Setting the value must be handled by the delegate. Unused if this option does not implement a button widget. + /// + /// The pawn being displayed when the button was clicked. + /// Currently unused but as practice return true if the value was changed. + public delegate bool OnButtonClickHandler(Pawn pawn); + + /// + /// Called when the button for this option is clicked in the default rules dialog. Setting the value must be handled by the delegate. Unused if this option does not implement a button widget. + /// + /// The target of the default rule. + /// Currently unused but as practice return true if the value was changed. + public delegate bool OnDefaultButtonClickHandler(OptionTarget target); + + /// + /// Called when the button for this option is clicked. Unused if this option does not implement a button widget. + /// + public OnButtonClickHandler OnButtonClick { get; set; } + + /// + /// Called when the button for this option is clicked in the default rules dialog. Unused if this option does not implement a button widget. + /// + public OnDefaultButtonClickHandler OnDefaultButtonClick { get; set; } + internal AddonOption Addon { get; } + + internal OptionHandle(AddonOption addon) => Addon = addon; + + /// + /// Used to see if a given pawn has this option. + /// + /// The pawn to query. + /// + public bool IsUsedBy(Pawn pawn) + { + var rules = Registry.GetRules(pawn); + return (rules != null) && rules.HasAddon(Addon); + } + + internal void ChangeValue(Pawn pawn, T newValue) + { + var handle = this as OptionHandle; + handle.SetValue(pawn, handle.OnChangeForPawnForPawn == null ? newValue : handle.OnChangeForPawnForPawn(pawn, handle.GetValue(pawn), newValue)); + } + + internal void ChangePresetValue(Rules rules, T newValue) + { + var handle = this as OptionHandle; + rules.SetAddonValueDirect(handle.Addon, handle.OnChangeForForPreset == null ? newValue : handle.OnChangeForForPreset(rules.Type.AsTarget, rules.GetAddonValue(handle.Addon, (T) handle.Addon.DefaultValue), newValue)); + } + + internal void DoClick(Pawn pawn) => OnButtonClick(pawn); + internal void DoDefaultClick(OptionTarget target) => OnDefaultButtonClick(target); + } + + /// + /// Provides a handle for a rules option. This class may not be instantiated manually, create a to add one. + /// + /// The value type. + public class OptionHandle : OptionHandle + { + /// + /// This is called when the value of the option is changing for a pawn. Unused if the option implements a button. + /// + /// The pawn whose rule option is being changed. + /// The original value of the option. + /// The new value of the option entered in the rules dialog. + /// The return value will be the value set for the option. + public delegate T OnChangeForPawnHandler(Pawn pawn, T oldValue, T inputValue); + + /// + /// This is called when the value of the option is changing for a preset. Unused if the option implements a button. + /// + /// The target of the preset that is being changed. + /// The original value of the option. + /// The new value of the option entered in the rules dialog. + /// The return value will be the value set for the option. + public delegate T OnChangeForPresetHandler(OptionTarget target, T oldValue, T inputValue); + + /// + /// This is called when the value of the option is changing for a pawn. Unused if option option implements a button. + /// + public OnChangeForPawnHandler OnChangeForPawnForPawn { get; set; } + + /// + /// This is called when the value of the option is changing for a preset type. Unused if the option implements a button. + /// + public OnChangeForPresetHandler OnChangeForForPreset { get; set; } + + /// + /// Gets or sets the displayed label of this option. + /// + public string Label { get => Addon.Label; set => Addon.Label = value; } + + /// + /// Gets or sets the tooltip of this option. + /// + public string Tooltip { get => Addon.Tooltip; set => Addon.Tooltip = value; } + + internal OptionHandle(AddonOption addon) : base(addon) + { } + + /// + /// Gets the value of this option for the given pawn. + /// + /// The pawn to get the value from. + /// The value returned if unable to retrieve the option. + /// Returns the value if the option is found or if not. + public T GetValue(Pawn pawn, T invalidValue = default(T)) + { + var rules = Registry.GetRules(pawn); + return rules == null ? invalidValue : rules.GetAddonValue(Addon, invalidValue); + } + + /// + /// Gets the default value of this option for the given target. + /// + /// The default rules target to get the value from. + /// The value returned if unable to retrieve the option. + /// Returns the value if the option is found or if not. + public T GetDefaultValue(OptionTarget target, T invalidValue = default(T)) => Registry.GetAddonDefaultValue(target, Addon); + + /// + /// Sets the value of this option for the given pawn. + /// + /// The pawn to set the value to. + /// The new value for the option. + /// Returns true if the option was successfully set. + public bool SetValue(Pawn pawn, T value) + { + var rules = Registry.GetRules(pawn); + return (rules != null) && rules.SetAddonValueDirect(Addon, value); + } + } +} diff --git a/Source/SDK/OptionTarget.cs b/Source/SDK/OptionTarget.cs new file mode 100644 index 0000000..57d15c1 --- /dev/null +++ b/Source/SDK/OptionTarget.cs @@ -0,0 +1,28 @@ +using System; + +namespace PawnRules.SDK +{ + /// + /// Used to set the target type of the pawn that a rule will be applied to. Multiple flags may be set. + /// + [Flags] + public enum OptionTarget + { + /// + /// For all colonists part of the player's faction. + /// + Colonist = 1, + /// + /// For all animals part of the player's faction. + /// + Animal = 2, + /// + /// For all guests that are currently staying with the player's faction. + /// + Guest = 4, + /// + /// For all prisoners that are being held by the player's faction. + /// + Prisoner = 8 + } +} diff --git a/Source/SDK/PawnRulesLink.cs b/Source/SDK/PawnRulesLink.cs new file mode 100644 index 0000000..418b743 --- /dev/null +++ b/Source/SDK/PawnRulesLink.cs @@ -0,0 +1,127 @@ +using System.Linq; +using System.Reflection; +using PawnRules.Data; +using PawnRules.Interface; +using Verse; + +namespace PawnRules.SDK +{ + /// + /// Provides a link to Pawn Rules and is used to add options to the rules dialog. + /// + public class PawnRulesLink + { + internal readonly ModContentPack ModContentPack; + + /// + /// Initializes a link to Pawn Rules. Only one plugin per mod is allowed. + /// + public PawnRulesLink() + { + if (Registry.IsActive || !AddonManager.AcceptingAddons) { throw new Mod.Exception("Link must be created before a world is loaded"); } + + var assembly = Assembly.GetCallingAssembly(); + var modContentPack = LoadedModManager.RunningMods.FirstOrDefault(mod => mod.assemblies.loadedAssemblies.Contains(assembly)) ?? throw new Mod.Exception("Assembly not a registered RimWorld mod"); + if (AddonManager.Mods.Contains(modContentPack)) { throw new Mod.Exception("Only one plugin per mod is allowed"); } + + ModContentPack = modContentPack; + Mod.Log($"Registered with {ModContentPack.Identifier}"); + } + + /// + /// Adds a new Toggle to the Rules dialog that sets a . + /// + /// The key used in the save file. Will be automatically prefixed with your Mod Identifier. + /// The type(s) of pawns this option will apply to. + /// The label of the widget displayed. + /// The tooltip displayed for the widget. + /// This is the default value if is false or no default rules exist when a pawn is first given rules. + /// If set to false it cannot be used in a preset. + /// Returns a handle for this option. + public OptionHandle AddToggle(string key, OptionTarget target, string label, string tooltip, bool defaultValue, bool allowedInPreset = true) => AddonOption.Add(this, key, target, OptionWidget.Checkbox, label, tooltip, defaultValue, allowedInPreset); + + /// + /// Adds a new Entry to the Rules dialog that sets a value. + /// + /// The key used in the save file. Will be automatically prefixed with your Mod Identifier. + /// The type(s) of pawns this option will apply to. + /// The label of the widget displayed. + /// The tooltip displayed for the widget. + /// This is the default value if is false or no default rules exist when a pawn is first given rules. + /// If set to false it cannot be used in a preset. + /// Returns a handle for this option. + public OptionHandle AddEntry(string key, OptionTarget target, string label, string tooltip, string defaultValue, bool allowedInPreset = true) => AddonOption.Add(this, key, target, OptionWidget.TextEntry, label, tooltip, defaultValue, allowedInPreset); + + /// + /// Adds a new Entry to the Rules dialog that sets an value. + /// + /// The key used in the save file. Will be automatically prefixed with your Mod Identifier. + /// The type(s) of pawns this option will apply to. + /// The label of the widget displayed. + /// The tooltip displayed for the widget. + /// This is the default value if is false or no default rules exist when a pawn is first given rules. + /// If set to false it cannot be used in a preset. + /// Returns a handle for this option. + public OptionHandle AddEntry(string key, OptionTarget target, string label, string tooltip, int defaultValue, bool allowedInPreset = true) => AddonOption.Add(this, key, target, OptionWidget.TextEntry, label, tooltip, defaultValue, allowedInPreset); + + /// + /// Adds a new Entry to the Rules dialog that sets a value. + /// + /// The key used in the save file. Will be automatically prefixed with your Mod Identifier. + /// The type(s) of pawns this option will apply to. + /// The label of the widget displayed. + /// The tooltip displayed for the widget. + /// This is the default value if is false or no default rules exist when a pawn is first given rules. + /// If set to false it cannot be used in a preset. + /// Returns a handle for this option. + public OptionHandle AddEntry(string key, OptionTarget target, string label, string tooltip, float defaultValue, bool allowedInPreset = true) => AddonOption.Add(this, key, target, OptionWidget.TextEntry, label, tooltip, defaultValue, allowedInPreset); + + /// + /// Adds a new Button to the Rules dialog that sets a value. Buttons require to be used to set the value. + /// + /// The key used in the save file. Will be automatically prefixed with your Mod Identifier. + /// The type(s) of pawns this option will apply to. + /// The label of the widget displayed. + /// The tooltip displayed for the widget. + /// This is the default value if is false or no default rules exist when a pawn is first given rules. + /// If set to false it cannot be used in a preset. + /// Returns a handle for this option. + public OptionHandle AddButton(string key, OptionTarget target, string label, string tooltip, string defaultValue, bool allowedInPreset = true) => AddonOption.Add(this, key, target, OptionWidget.Button, label, tooltip, defaultValue, allowedInPreset); + + /// + /// Adds a new Button to the Rules dialog that sets a value. Buttons require to be used to set the value. + /// + /// The key used in the save file. Will be automatically prefixed with your Mod Identifier. + /// The type(s) of pawns this option will apply to. + /// The label of the widget displayed. + /// The tooltip displayed for the widget. + /// This is the default value if is false or no default rules exist when a pawn is first given rules. + /// If set to false it cannot be used in a preset. + /// Returns a handle for this option. + public OptionHandle AddButton(string key, OptionTarget target, string label, string tooltip, bool defaultValue, bool allowedInPreset = true) => AddonOption.Add(this, key, target, OptionWidget.Button, label, tooltip, defaultValue, allowedInPreset); + + /// + /// Adds a new Button to the Rules dialog that sets an value. Buttons require to be used to set the value. + /// + /// The key used in the save file. Will be automatically prefixed with your Mod Identifier. + /// The type(s) of pawns this option will apply to. + /// The label of the widget displayed. + /// The tooltip displayed for the widget. + /// This is the default value if is false or no default rules exist when a pawn is first given rules. + /// If set to false it cannot be used in a preset. + /// Returns a handle for this option. + public OptionHandle AddButton(string key, OptionTarget target, string label, string tooltip, int defaultValue, bool allowedInPreset = true) => AddonOption.Add(this, key, target, OptionWidget.Button, label, tooltip, defaultValue, allowedInPreset); + + /// + /// Adds a new Button to the Rules dialog that sets a value. Buttons to be used to set the value. + /// + /// The key used in the save file. Will be automatically prefixed with your Mod Identifier. + /// The type(s) of pawns this option will apply to. + /// The label of the widget displayed. + /// The tooltip displayed for the widget. + /// This is the default value if is false or no default rules exist when a pawn is first given rules. + /// If set to false it cannot be used in a preset. + /// Returns a handle for this option. + public OptionHandle AddButton(string key, OptionTarget target, string label, string tooltip, float defaultValue, bool allowedInPreset = true) => AddonOption.Add(this, key, target, OptionWidget.Button, label, tooltip, defaultValue, allowedInPreset); + } +} diff --git a/Textures/UI/EditRules.png b/Textures/UI/EditRules.png new file mode 100644 index 0000000000000000000000000000000000000000..143a9b8ab12c2a0ec0a327b3e7600bbbe89d58f3 GIT binary patch literal 2088 zcmV+@2-o+CP)@8efxA|eA6mG6CO8v zYonl`An7o8yg7g?MBnEF0|QxCSC>Tz?B?diu3x|YO0ZWl*%}RmNchgqPRl}g_7lT+ z#v(yi3qiN-W`n|5M?0p2l)x+G%lpMitwjVo>1g+u)L3AOg#QRdt*nWJ4n=z)${ki6 z3&@4vy?Zx%@ZbR}D=TBbF4XiH_!Em)0EEl{*Ma-NT5y!u9wG_9ckf=7k&(eXJUm!v zXec{$=#as6-fS*_&N!j)QYh;nxEa1XD!Xr?IFb@Y5TpGJ%AM4(XU`sX?AS4#8;y*N zj2#+{mt|16hTvjNO^ptegq9^3=>mQiTq4%h;!N@Raa0Ei73%Bjg}%N%;rQ|60)!ts z7s4Bax{DVtmd@EqrI(kN&N&qo71I9|a+yuBfFiK}!i5VN9-?UY2L=XMOG^uNWG95Y zfu;b-yhB1lm;}p*hlkmbBS%ohrigfR4NilLtPfuq{mo8-q3A%yY!-o&q`t|F%5;3@0Bg)`mj0+gM3f!Wf+qZA4 zHsd2sXHTC#m8#`r6y4q3EIK-xAtX0=&66ij_{iVAdzY%E;H{WQdnY3)%1!j>(WBf< z0|EjR3E$t}&rY8{&4z}CsQMd(pj9Ce^MM0=`t!)hNOtw=RW^VA{IP3#dU{xRcsOfr zZl-FPDEu=;Sra2yfgF`r0OBgJo2(ym!CLwcmLQCdj`DtBsS_Z)bj=m8v%LSNxPU3K zz}Fa|^CIAQPoW3&ix~cb-ZKX9jO{qQQc_alL*aN_TpXV;vEJPHC7rb%;fa4<#;iejxI7=nEF1($c9m~nd;lU8C z7Qw+h(-oozqb*MX5YP);Onyx(E@6ArGsXYT$c0Z%PUhDTD&4hf*Q5tS`UAx9enE*RG#2lZ5^2TW6#*KW7%imwu<7#VbS!`@9N0gvkxY9Ob zbArZBf_#FBiHT8d(xVLvF`dOEY;o$;DQ+2Z(igt2u8#Ku)f2^X5&4$WE%#R`&N4|0gCUvdYRzgV&HXY}l}Y#|5B^5I$K+4&4|kp`YyRY`*@g z5e(($=S%6VYzQ(;%N)$u9s$xe@@?APktbd#_K%8+V$YuO_~&O_^M#^Ln6L_Dnjska z1#F|&my7hDpP!!qMe%<0_Vx-sK0eYldHR9SWCcSSjL?3Sf}uVU2N;^U3D-L#_z8D% za$-;f&qH9cr2n)M6jn;621C7K3hJ^M7#(t@E@F(pp@ zB+zjSZ6j(v!y>lE#L4kbbKHBzzb0pr1&BMdYot8B5;QEv_yrcD00^H=a9zbde2aTh zax(#n=$h^dpzY^0diz47(Wnv(5vT(8|CDBXaQt*v0MEZ2Ja~{DK73e}4#daDGhbg{ z_Wb$tI9S44u!5nN43DU{Wx2?%X-{^5siaI#5(p zBoBrjO?Mag3H*&#tMy*CY#Ad~8VpfBKzv{MjQl-;>AVa^(u9RkTPGVirIbZ6hD9uc z=qT;Ve1Zv!vKYQ9Bt;w5hPRUfh0*CpQ{l5ytpIytb8gnAfOckOG$94hi^mgv_jbAv z_1P2!@byUlGaC6@*Op3RCW#-82ZSgq%d@W5M%C5TI$-YAkn1KCH&f!!)6uYGu1eIvXg46fhUX4^t7E znwfG9?H-Ny;Uw(?QtCUHS#IiVRP=euIvL!~vQhf+^#9)uTR7Rw^v~!25nuova1fH( S7iSOv0000