Decompiled source of ProperlyPersistentProfilesPatchPlugin v1.0.0

PersistentProfiles.dll

Decompiled a year ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HG;
using HG.Reflection;
using IL.RoR2;
using Mono.Cecil.Cil;
using MonoMod.Cil;
using On.RoR2;
using On.RoR2.Stats;
using RoR2;
using RoR2.Stats;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: OptIn]
[assembly: TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName = "")]
[assembly: AssemblyCompany("PersistentProfiles")]
[assembly: AssemblyConfiguration("Debug")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("PersistentProfiles")]
[assembly: AssemblyTitle("PersistentProfiles")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.0.0.0")]
[module: UnverifiableCode]
namespace PersistentProfiles;

public static class BodyLoadouts
{
	public static Dictionary<UserProfile, XElement[]> orphanedBodyLoadoutsLookup;

	public static void Init()
	{
		//IL_0012: Unknown result type (might be due to invalid IL or missing references)
		//IL_001c: Expected O, but got Unknown
		//IL_0024: Unknown result type (might be due to invalid IL or missing references)
		//IL_002e: Expected O, but got Unknown
		//IL_0036: Unknown result type (might be due to invalid IL or missing references)
		//IL_0040: Expected O, but got Unknown
		orphanedBodyLoadoutsLookup = new Dictionary<UserProfile, XElement[]>();
		XmlUtility.ToXml += new hook_ToXml(XmlUtility_ToXml);
		XmlUtility.FromXml += new hook_FromXml(XmlUtility_FromXml);
		SaveSystem.Copy += new hook_Copy(SaveSystem_Copy);
	}

	private static XDocument XmlUtility_ToXml(orig_ToXml orig, UserProfile userProfile)
	{
		XDocument xDocument = orig.Invoke(userProfile);
		if (orphanedBodyLoadoutsLookup.TryGetValue(userProfile, out var value) && TryFindBodyLoadoutsElement(xDocument, out var bodyLoadoutsElement))
		{
			XElement xElement = bodyLoadoutsElement;
			object[] content = value;
			xElement.Add(content);
		}
		return xDocument;
	}

	private static UserProfile XmlUtility_FromXml(orig_FromXml orig, XDocument doc)
	{
		UserProfile val = orig.Invoke(doc);
		if (TryFindBodyLoadoutsElement(doc, out var bodyLoadoutsElement))
		{
			XElement[] array = (from x in bodyLoadoutsElement.Elements("BodyLoadout")
				group x by x.Attribute("bodyName")?.Value into x
				where x.Key != null && (int)BodyCatalog.FindBodyIndex(x.Key) == -1
				select x.First()).ToArray();
			if (array.Length != 0)
			{
				orphanedBodyLoadoutsLookup[val] = array;
			}
		}
		return val;
	}

	public static bool TryFindBodyLoadoutsElement(XDocument doc, out XElement bodyLoadoutsElement)
	{
		return (bodyLoadoutsElement = doc?.Root?.Element("loadout")?.Element("BodyLoadouts")) != null;
	}

	private static void SaveSystem_Copy(orig_Copy orig, UserProfile src, UserProfile dest)
	{
		orig.Invoke(src, dest);
		if (orphanedBodyLoadoutsLookup.TryGetValue(src, out var value))
		{
			orphanedBodyLoadoutsLookup[dest] = value;
		}
	}
}
public static class Eclipse
{
	public const string eclipseString = "Eclipse.";

	public const int maxVanillaEclipseLevel = 8;

	public static bool ignoreModdedEclipse;

	private static int restoringUnlockableCount;

	public static void Init()
	{
		//IL_002c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0036: Expected O, but got Unknown
		PersistentProfiles.onUntrustedProfileDiscovered += PersistentProfiles_onUntrustedProfileDiscovered;
		UserProfile.onUnlockableGranted += UserProfile_onUnlockableGranted;
		UserProfile.RevokeUnlockable += new hook_RevokeUnlockable(UserProfile_RevokeUnlockable);
	}

	private static void PersistentProfiles_onUntrustedProfileDiscovered(UserProfile userProfile, XDocument doc)
	{
		RestoreEclipseUnlockables(userProfile, out var survivorNameToPersistentEclipseLevel);
		if (TryFindStatsElement(doc, out var statsElement))
		{
			UpdateAllPersistentEclipseUnlockables(userProfile, from x in statsElement.Elements("unlock")
				select (x.Nodes().FirstOrDefault((XNode node) => node.NodeType == XmlNodeType.Text) as XText)?.Value into x
				where x?.StartsWith("Eclipse.") ?? false
				select x, survivorNameToPersistentEclipseLevel);
		}
		userProfile.saveRequestPending = true;
	}

	public static void RestoreEclipseUnlockables(UserProfile userProfile, out Dictionary<string, int> survivorNameToPersistentEclipseLevel)
	{
		PersistentProfiles.logger.LogInfo((object)("Restoring eclipse unlockables for UserProfile " + userProfile.name + ":"));
		survivorNameToPersistentEclipseLevel = new Dictionary<string, int>();
		foreach (string achievements in userProfile.achievementsList)
		{
			if (achievements.StartsWith("Eclipse.") && TryParseEclipseUnlockable(achievements, out var survivorName, out var eclipseLevel))
			{
				if (survivorNameToPersistentEclipseLevel.TryGetValue(survivorName, out var value))
				{
					survivorNameToPersistentEclipseLevel[survivorName] = Math.Max(value, eclipseLevel);
				}
				else
				{
					survivorNameToPersistentEclipseLevel.Add(survivorName, eclipseLevel);
				}
			}
		}
		StringBuilder stringBuilder = StringBuilderPool.RentStringBuilder();
		foreach (KeyValuePair<string, int> item in survivorNameToPersistentEclipseLevel)
		{
			int num = item.Value;
			if (ignoreModdedEclipse)
			{
				num = Math.Min(num, 9);
			}
			PersistentProfiles.logger.LogInfo((object)$"Restoring unlockables up to Eclipse {num} for {item.Key}.");
			for (int i = EclipseRun.minUnlockableEclipseLevel; i <= num; i++)
			{
				stringBuilder.Clear();
				StringBuilderExtensions.AppendInt(stringBuilder.Append("Eclipse.").Append(item.Key).Append("."), i, 1u, uint.MaxValue);
				RestoreEclipseUnlockable(userProfile, stringBuilder.ToString());
			}
		}
		StringBuilderPool.ReturnStringBuilder(stringBuilder);
	}

	public static void UpdateAllPersistentEclipseUnlockables(UserProfile userProfile, IEnumerable<string> eclipseUnlockableNames, Dictionary<string, int> survivorNameToPersistentEclipseLevel)
	{
		HashSet<string> hashSet = new HashSet<string>();
		foreach (string eclipseUnlockableName in eclipseUnlockableNames)
		{
			if (TryParseEclipseUnlockable(eclipseUnlockableName, out var survivorName, out var eclipseLevel) && hashSet.Add(survivorName) && (!survivorNameToPersistentEclipseLevel.TryGetValue(survivorName, out var value) || eclipseLevel > value))
			{
				UpdatePersistentEclipseUnlockables(userProfile, survivorName);
			}
		}
	}

	public static bool TryFindStatsElement(XDocument doc, out XElement statsElement)
	{
		return (statsElement = doc?.Root?.Element("stats")) != null;
	}

	public static void RestoreEclipseUnlockable(UserProfile userProfile, string eclipseUnlockableName)
	{
		UnlockableDef unlockableDef = UnlockableCatalog.GetUnlockableDef(eclipseUnlockableName);
		if (Object.op_Implicit((Object)(object)unlockableDef) && !userProfile.HasUnlockable(unlockableDef))
		{
			restoringUnlockableCount++;
			try
			{
				userProfile.GrantUnlockable(unlockableDef);
				return;
			}
			finally
			{
				restoringUnlockableCount--;
			}
		}
		if (userProfile.statSheet != null)
		{
			if (!Stats.orphanedUnlocksLookup.TryGetValue(userProfile.statSheet, out var value))
			{
				Stats.orphanedUnlocksLookup.Add(userProfile.statSheet, value = new HashSet<string>());
			}
			value.Add(eclipseUnlockableName);
		}
	}

	private static void UserProfile_onUnlockableGranted(UserProfile userProfile, UnlockableDef unlockableDef)
	{
		if (userProfile != null && Object.op_Implicit((Object)(object)unlockableDef) && unlockableDef.cachedName.StartsWith("Eclipse.") && TryParseEclipseUnlockable(unlockableDef.cachedName, out var survivorName, out var _))
		{
			UpdatePersistentEclipseUnlockables(userProfile, survivorName);
		}
	}

	private static void UserProfile_RevokeUnlockable(orig_RevokeUnlockable orig, UserProfile userProfile, UnlockableDef unlockableDef)
	{
		orig.Invoke(userProfile, unlockableDef);
		if (userProfile != null && Object.op_Implicit((Object)(object)unlockableDef) && unlockableDef.cachedName.StartsWith("Eclipse.") && TryParseEclipseUnlockable(unlockableDef.cachedName, out var survivorName, out var _))
		{
			UpdatePersistentEclipseUnlockables(userProfile, survivorName);
		}
	}

	public static void UpdatePersistentEclipseUnlockables(UserProfile userProfile, string survivorName)
	{
		if (restoringUnlockableCount > 0)
		{
			return;
		}
		if (string.IsNullOrWhiteSpace(survivorName) || survivorName.Contains(" "))
		{
			PersistentProfiles.logger.LogWarning((object)("Cannot add a persistent eclipse unlockable for invalid survivor name '" + survivorName + "'! Ignoring."));
			return;
		}
		SurvivorDef val = SurvivorCatalog.FindSurvivorDef(survivorName);
		if (!((Object)(object)val == (Object)null))
		{
			List<UnlockableDef> eclipseLevelUnlockablesForSurvivor = EclipseRun.GetEclipseLevelUnlockablesForSurvivor(val);
			int num = eclipseLevelUnlockablesForSurvivor.Count;
			if (ignoreModdedEclipse)
			{
				num = Math.Min(num, 8);
			}
			int num2 = eclipseLevelUnlockablesForSurvivor.FindLastIndex((UnlockableDef x) => userProfile.HasUnlockable(x));
			PersistentProfiles.logger.LogInfo((object)$"Updating persistent eclipse unlockable for {survivorName} in UserProfile {userProfile.name}. Highest eclipse unlockable is Eclipse {EclipseRun.minUnlockableEclipseLevel + num2}.");
			for (int i = 0; i < num; i++)
			{
				userProfile.achievementsList.Remove(eclipseLevelUnlockablesForSurvivor[i].cachedName);
			}
			if (num2 >= 0 && num2 < num)
			{
				userProfile.achievementsList.Add(eclipseLevelUnlockablesForSurvivor[num2].cachedName);
			}
			userProfile.RequestEventualSave();
		}
	}

	public static bool TryParseEclipseUnlockable(string eclipseUnlockableString, out string survivorName, out int eclipseLevel)
	{
		int num = 7;
		int num2 = eclipseUnlockableString.LastIndexOf('.');
		if (num == num2 || !int.TryParse(eclipseUnlockableString.Substring(num2 + 1), out eclipseLevel))
		{
			survivorName = null;
			eclipseLevel = 0;
			return false;
		}
		survivorName = eclipseUnlockableString.Substring(num + 1, num2 - num - 1);
		return true;
	}
}
[BepInPlugin("com.groovesalad.PersistentProfiles", "PersistentProfiles", "1.0.0")]
public class PersistentProfiles : BaseUnityPlugin
{
	private const string configSection = "PersistentProfiles";

	private const string trustedProfileFlag = "GS_PersistentProfiles_Trusted";

	public static ManualLogSource logger;

	public static PersistentProfiles instance;

	public static ConfigFile config;

	public static event Action<UserProfile, XDocument> onUntrustedProfileDiscovered;

	public void Awake()
	{
		//IL_002d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0037: Expected O, but got Unknown
		//IL_010c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0116: Expected O, but got Unknown
		//IL_011e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0128: Expected O, but got Unknown
		logger = ((BaseUnityPlugin)this).Logger;
		instance = this;
		config = new ConfigFile(Path.Combine(Paths.ConfigPath, "PersistentProfiles.cfg"), true, ((BaseUnityPlugin)this).Info.Metadata);
		Stats.includeStats = config.Bind<bool>("PersistentProfiles", "Preserve Modded Stats", true, "Prevent modded stats from being wiped for as long as this mod is installed.").Value;
		Stats.includeAllUnlocks = config.Bind<bool>("PersistentProfiles", "Preserve Modded Unlockables", true, "Prevent modded unlockables that are not tied to achievements (like stage and monster logs) from being wiped for as long as this mod is installed. Eclipse unlocks are always saved.").Value;
		Stats.Init();
		Eclipse.ignoreModdedEclipse = config.Bind<bool>("PersistentProfiles", "Ignore Extended Eclipse Levels", true, "Only manage Eclipse 8 and below to avoid possible conflicts with mods that add new eclipse levels.").Value;
		Eclipse.Init();
		if (config.Bind<bool>("PersistentProfiles", "Preserve Modded Loadouts", true, "Prevent modded loadout preferences from being wiped for as long as this mod is installed.").Value)
		{
			BodyLoadouts.Init();
		}
		if (config.Bind<bool>("PersistentProfiles", "Preserve Modded Pickups", true, "Prevent discovered modded items and equipment from being wiped for as long as this mod is installed.").Value)
		{
			Pickups.Init();
		}
		XmlUtility.ToXml += new hook_ToXml(XmlUtility_ToXml);
		XmlUtility.FromXml += new hook_FromXml(XmlUtility_FromXml);
	}

	private XDocument XmlUtility_ToXml(orig_ToXml orig, UserProfile userProfile)
	{
		XDocument xDocument = orig.Invoke(userProfile);
		if (xDocument?.Root != null)
		{
			xDocument.Root.Add(new XElement("GS_PersistentProfiles_Trusted"));
		}
		return xDocument;
	}

	private UserProfile XmlUtility_FromXml(orig_FromXml orig, XDocument doc)
	{
		UserProfile val = orig.Invoke(doc);
		if (val != null && doc?.Root != null && doc.Root.Element("GS_PersistentProfiles_Trusted") == null)
		{
			PersistentProfiles.onUntrustedProfileDiscovered?.Invoke(val, doc);
			((MonoBehaviour)this).StartCoroutine(SaveUntrustedProfile(val));
		}
		return val;
	}

	public IEnumerator SaveUntrustedProfile(UserProfile userProfile)
	{
		yield return (object)new WaitForFixedUpdate();
		if (userProfile?.canSave ?? false)
		{
			SaveSystem saveSystem = PlatformSystems.saveSystem;
			if (saveSystem != null)
			{
				saveSystem.Save(userProfile, false);
			}
		}
	}
}
public static class Pickups
{
	public static Dictionary<UserProfile, string> orphanedPickupsLookup;

	public static void Init()
	{
		//IL_0012: Unknown result type (might be due to invalid IL or missing references)
		//IL_001c: Expected O, but got Unknown
		orphanedPickupsLookup = new Dictionary<UserProfile, string>();
		SaveFieldAttribute.SetupPickupsSet += new hook_SetupPickupsSet(SaveFieldAttribute_SetupPickupsSet);
		if (UserProfile.saveFields == null)
		{
			return;
		}
		SaveFieldAttribute[] saveFields = UserProfile.saveFields;
		foreach (SaveFieldAttribute val in saveFields)
		{
			if (val.explicitSetupMethod == "SetupPickupsSet")
			{
				ModifyPickupsSaveField(val);
			}
		}
	}

	private static void SaveFieldAttribute_SetupPickupsSet(orig_SetupPickupsSet orig, SaveFieldAttribute self, FieldInfo fieldInfo)
	{
		orig.Invoke(self, fieldInfo);
		ModifyPickupsSaveField(self);
	}

	public static void ModifyPickupsSaveField(SaveFieldAttribute saveField)
	{
		Func<UserProfile, string> origGetter = saveField.getter;
		saveField.getter = delegate(UserProfile userProfile)
		{
			string text2 = origGetter(userProfile);
			if (orphanedPickupsLookup.TryGetValue(userProfile, out var value2))
			{
				PersistentProfiles.logger.LogInfo((object)("Getting orphaned pickups: " + value2));
				text2 = text2 + " " + value2;
			}
			return text2;
		};
		saveField.setter = (Action<UserProfile, string>)Delegate.Combine(saveField.setter, (Action<UserProfile, string>)delegate(UserProfile userProfile, string valueString)
		{
			string text = string.Join(" ", valueString.Split(new char[1] { ' ' }).Where(delegate(string x)
			{
				//IL_0001: Unknown result type (might be due to invalid IL or missing references)
				//IL_0006: Unknown result type (might be due to invalid IL or missing references)
				PickupIndex val = PickupCatalog.FindPickupIndex(x);
				return !((PickupIndex)(ref val)).isValid;
			}).Distinct());
			if (!string.IsNullOrWhiteSpace(text))
			{
				orphanedPickupsLookup[userProfile] = text;
				PersistentProfiles.logger.LogInfo((object)("Orphaned pickups for UserProfile " + userProfile.name + ": " + text));
			}
		});
		saveField.copier = (Action<UserProfile, UserProfile>)Delegate.Combine(saveField.copier, (Action<UserProfile, UserProfile>)delegate(UserProfile srcProfile, UserProfile destProfile)
		{
			if (orphanedPickupsLookup.TryGetValue(srcProfile, out var value))
			{
				orphanedPickupsLookup[destProfile] = value;
			}
		});
	}
}
public static class Stats
{
	public static bool includeStats;

	public static bool includeAllUnlocks;

	public static Dictionary<StatSheet, Dictionary<string, string>> orphanedStatsLookup;

	public static Dictionary<StatSheet, HashSet<string>> orphanedUnlocksLookup;

	public static void Init()
	{
		//IL_0027: Unknown result type (might be due to invalid IL or missing references)
		//IL_0031: Expected O, but got Unknown
		//IL_0039: Unknown result type (might be due to invalid IL or missing references)
		//IL_0043: Expected O, but got Unknown
		//IL_004b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0055: Expected O, but got Unknown
		if (includeStats)
		{
			orphanedStatsLookup = new Dictionary<StatSheet, Dictionary<string, string>>();
		}
		orphanedUnlocksLookup = new Dictionary<StatSheet, HashSet<string>>();
		XmlUtility.GetStatsField += new Manipulator(XmlUtility_GetStatsField);
		XmlUtility.CreateStatsField += new hook_CreateStatsField(XmlUtility_CreateStatsField);
		StatSheet.Copy += new hook_Copy(StatSheet_Copy);
	}

	private static void StatSheet_Copy(orig_Copy orig, StatSheet src, StatSheet dest)
	{
		orig.Invoke(src, dest);
		if (includeStats && orphanedStatsLookup.TryGetValue(src, out var value))
		{
			orphanedStatsLookup[dest] = value;
		}
		if (orphanedUnlocksLookup.TryGetValue(src, out var value2))
		{
			orphanedUnlocksLookup[dest] = value2;
		}
	}

	private static void XmlUtility_GetStatsField(ILContext il)
	{
		//IL_0002: Unknown result type (might be due to invalid IL or missing references)
		//IL_0008: Expected O, but got Unknown
		//IL_0161: Unknown result type (might be due to invalid IL or missing references)
		//IL_0180: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ad: Unknown result type (might be due to invalid IL or missing references)
		//IL_00b9: Unknown result type (might be due to invalid IL or missing references)
		//IL_00cb: Unknown result type (might be due to invalid IL or missing references)
		//IL_00dd: Unknown result type (might be due to invalid IL or missing references)
		ILCursor val = new ILCursor(il);
		if (includeStats)
		{
			int locStatNameIndex = -1;
			int locStatValueIndex = -1;
			if (val.TryGotoNext((MoveType)0, new Func<Instruction, bool>[4]
			{
				(Instruction x) => ILPatternMatchingExt.MatchLdloc(x, ref locStatNameIndex),
				(Instruction x) => ILPatternMatchingExt.MatchCallOrCallvirt<StatDef>(x, "Find"),
				(Instruction x) => ILPatternMatchingExt.MatchLdloc(x, ref locStatValueIndex),
				(Instruction x) => ILPatternMatchingExt.MatchCallOrCallvirt<StatSheet>(x, "SetStatValueFromString")
			}))
			{
				val.Index += 2;
				val.Emit(OpCodes.Dup);
				val.Emit(OpCodes.Ldloc, locStatNameIndex);
				val.Emit(OpCodes.Ldloc, locStatValueIndex);
				val.Emit(OpCodes.Ldarg, 2);
				val.EmitDelegate<Action<StatDef, string, string, StatSheet>>((Action<StatDef, string, string, StatSheet>)delegate(StatDef statDef, string name, string value, StatSheet dest)
				{
					if (!string.IsNullOrEmpty(name) && value != null && statDef == null)
					{
						if (!orphanedStatsLookup.TryGetValue(dest, out var value3))
						{
							orphanedStatsLookup.Add(dest, value3 = new Dictionary<string, string>());
						}
						value3[name] = value;
					}
				});
			}
			else
			{
				PersistentProfiles.logger.LogError((object)"Failed stats IL hook for XmlUtility_GetStatsField!");
			}
		}
		if (val.TryGotoNext((MoveType)0, new Func<Instruction, bool>[1]
		{
			(Instruction x) => ILPatternMatchingExt.MatchCallOrCallvirt(x, typeof(UnlockableCatalog), "GetUnlockableDef")
		}))
		{
			val.MoveAfterLabels();
			val.Emit(OpCodes.Dup);
			int index = val.Index;
			val.Index = index + 1;
			val.Emit(OpCodes.Ldarg, 2);
			val.EmitDelegate<Func<string, UnlockableDef, StatSheet, UnlockableDef>>((Func<string, UnlockableDef, StatSheet, UnlockableDef>)delegate(string name, UnlockableDef unlockableDef, StatSheet dest)
			{
				if (!string.IsNullOrEmpty(name) && (Object)(object)unlockableDef == (Object)null && (includeAllUnlocks || name.StartsWith("Eclipse.")))
				{
					if (!orphanedUnlocksLookup.TryGetValue(dest, out var value2))
					{
						orphanedUnlocksLookup.Add(dest, value2 = new HashSet<string>());
					}
					value2.Add(name);
				}
				return unlockableDef;
			});
		}
		else
		{
			PersistentProfiles.logger.LogError((object)"Failed unlockables IL hook for XmlUtility_GetStatsField!");
		}
	}

	private static XElement XmlUtility_CreateStatsField(orig_CreateStatsField orig, string name, StatSheet statSheet)
	{
		XElement xElement = orig.Invoke(name, statSheet);
		if (includeStats && orphanedStatsLookup.TryGetValue(statSheet, out var value))
		{
			foreach (KeyValuePair<string, string> item in value)
			{
				XElement xElement2 = new XElement("stat", new XText(item.Value));
				xElement2.SetAttributeValue("name", item.Key);
				xElement.Add(xElement2);
			}
		}
		if (orphanedUnlocksLookup.TryGetValue(statSheet, out var value2))
		{
			foreach (string item2 in value2)
			{
				XElement content = new XElement("unlock", new XText(item2));
				xElement.Add(content);
			}
		}
		return xElement;
	}
}