diff --git a/Source/Mods/VanillaAspirationsExpanded.cs b/Source/Mods/VanillaAspirationsExpanded.cs new file mode 100644 index 0000000..1faef91 --- /dev/null +++ b/Source/Mods/VanillaAspirationsExpanded.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; +using Multiplayer.API; +using RimWorld; +using Verse; + +namespace Multiplayer.Compat; + +/// Vanilla Aspirations Expanded by Oskar Potocki, Legodude17, Sarg Bjornson +/// +/// +[MpCompatFor("VanillaExpanded.VanillaAspirationsExpanded")] +public class VanillaAspirationsExpanded +{ + #region Fields + + // MP Compat + private static bool isDrawingDialog = false; + + // List + private static Type aspirationDefListType; + + // SyncDelegates + private static FastInvokeHandler pickRandomTraitAndPassionsMethod; + + // ChoiceLetter_GrowthMoment_Aspirations + private static AccessTools.FieldRef aspirationChoicesField; + private static AccessTools.FieldRef aspirationGainsCountField; + private static FastInvokeHandler trySetAspirationChoicesMethod; + private static FastInvokeHandler makeAspirationChoicesMethod; + + #endregion + + #region Main Patch + + public VanillaAspirationsExpanded(ModContentPack mod) + { + // Run the patches + MpCompatPatchLoader.LoadPatch(this); + + // Setup list type with a generic we can't reference + var type = AccessTools.TypeByName("VAspirE.AspirationDef"); + aspirationDefListType = typeof(List<>).MakeGenericType(type); + + // Access MP method + type = AccessTools.TypeByName("Multiplayer.Client.SyncDelegates"); + var method = AccessTools.DeclaredMethod(type, "PickRandomTraitAndPassions"); + pickRandomTraitAndPassionsMethod = MethodInvoker.GetHandler(method); + + // Handle aspirations fulfilled letter/dialog + type = AccessTools.TypeByName("VAspirE.ChoiceLetter_AspirationsFulfilled"); + // This letter/dialog have different type, but are otherwise mostly the + // same as vanilla growth moment ones. MP should handle those already, + // and we only need to register the default choice for the letter. + MP.RegisterDefaultLetterChoice(method, type); + + // Handle growth moment with aspirations letter/dialog + type = AccessTools.TypeByName("VAspirE.ChoiceLetter_GrowthMoment_Aspirations"); + // Setup fields + aspirationChoicesField = AccessTools.FieldRefAccess(type, "aspirationChoices"); + aspirationGainsCountField = AccessTools.FieldRefAccess(type, "aspirationGainsCount"); + // Setup methods + trySetAspirationChoicesMethod = MethodInvoker.GetHandler(AccessTools.DeclaredMethod(type, "TrySetAspirationChoices")); + var makeAspirationChoices = AccessTools.DeclaredMethod(type, "MakeAspirationChoices"); + makeAspirationChoicesMethod = MethodInvoker.GetHandler(makeAspirationChoices); + // Sync methods + MP.RegisterSyncMethod(makeAspirationChoices); + // Letter timeout handling + MP.RegisterDefaultLetterChoice(MpMethodUtil.MethodOf(PickRandomAspirationsTraitPassions), type); + } + + #endregion + + #region Choice letter expiry handling + + private static void PickRandomAspirationsTraitPassions(ChoiceLetter_GrowthMoment letter) + { + // Make sure the aspirations are prepared + trySetAspirationChoicesMethod(letter); + + var aspirationChoices = aspirationChoicesField(letter); + var aspirationCount = aspirationGainsCountField(letter); + + // If there's any aspirations to pick from, pick some random ones. + if (aspirationCount > 0 && aspirationChoices != null) + makeAspirationChoicesMethod(letter, + Activator.CreateInstance(aspirationDefListType, aspirationChoices.Cast().InRandomOrder().Take(aspirationCount))); + // This is technically no-op due to null check, but include it just in case + // the mod decides to do something with it at some point (or a different mod uses it). + else + makeAspirationChoicesMethod(letter, null); + + // MP method to pick traits and passions, which handles normal, vanilla growth moment. + // This will also handle closing the letter. + pickRandomTraitAndPassionsMethod(letter); + } + + #endregion + + #region Seed aspiration generation RNG + + // The letter tries to generate the options when opened. Make the picks seeded, so all players will get the same ones. + [MpCompatPrefix("VAspirE.ChoiceLetter_GrowthMoment_Aspirations", "TrySetAspirationChoices")] + private static void PreTrySetAspirationChoices(ChoiceLetter_GrowthMoment __instance) + => Rand.PushState(Gen.HashCombineInt(__instance.pawn.thingIDNumber, __instance.arrivalTick)); + + [MpCompatPostfix("VAspirE.ChoiceLetter_GrowthMoment_Aspirations", "TrySetAspirationChoices")] + private static void PostTrySetAspirationChoices() + => Rand.PopState(); + + #endregion + + #region Don't close dialog when drawing it + + // Patches to not remove the letter if it's in the process of being drawn. + // Rather than prefixing LetterStack.RemoveLetter we could instead change + // Multiplayer.IsDrawingGrowthMomentDialog.isDrawing to true/false, and let + // MP handle this itself. However, it's safer to do a new patch for this. + + [MpCompatPrefix("VAspirE.Dialog_GrowthMomentChoices_Aspirations", nameof(Dialog_GrowthMomentChoices.DoWindowContents))] + private static void PreDoWindowContents() => isDrawingDialog = true; + + [MpCompatFinalizer("VAspirE.Dialog_GrowthMomentChoices_Aspirations", nameof(Dialog_GrowthMomentChoices.DoWindowContents))] + private static void PostDoWindowContents() => isDrawingDialog = false; + + [MpCompatPrefix(typeof(LetterStack), nameof(LetterStack.RemoveLetter))] + private static bool DontRemoveChoiceLetter() => !MP.IsInMultiplayer || !isDrawingDialog; + + #endregion +} \ No newline at end of file