Decompiled source of Inkorporated v1.1.0

Inkorporated.dll

Decompiled 20 hours ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using HarmonyLib;
using Il2CppInterop.Runtime.InteropTypes;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using Il2CppScheduleOne.UI.CharacterCustomization;
using Il2CppTMPro;
using Inkorporated;
using Inkorporated.Config;
using Inkorporated.Content;
using Inkorporated.Model;
using Inkorporated.Registration;
using Inkorporated.Shop;
using MelonLoader;
using MelonLoader.Preferences;
using MelonLoader.Utils;
using Microsoft.CodeAnalysis;
using Newtonsoft.Json;
using S1API.Rendering;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: MelonInfo(typeof(Core), "Inkorporated", "1.1.0", "DooDesch", "https://github.com/DooDesch-Mods/ScheduleOne-Inkorporated")]
[assembly: MelonGame("TVGS", "Schedule I")]
[assembly: MelonOptionalDependencies(new string[] { "ModManager&PhoneApp" })]
[assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")]
[assembly: AssemblyCompany("Inkorporated")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.1.0.0")]
[assembly: AssemblyInformationalVersion("1.1.0+4b11ddb24c640398473cc451a6cb1843bf120d9a")]
[assembly: AssemblyProduct("Inkorporated")]
[assembly: AssemblyTitle("Inkorporated")]
[assembly: NeutralResourcesLanguage("en-US")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.1.0.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace Inkorporated
{
	public static class API
	{
		public static bool RegisterTattoo(string id, string displayName, TattooPlacement placement, Texture2D texture, float price = 0f, string source = "API")
		{
			return TattooRegistry.Add(new TattooDef
			{
				Id = id,
				DisplayName = (string.IsNullOrWhiteSpace(displayName) ? id : displayName),
				Placement = placement,
				Price = ((price < 0f) ? 0f : price),
				Texture = texture,
				Source = (string.IsNullOrWhiteSpace(source) ? "API" : source)
			});
		}

		public static bool RegisterTattooFromFile(string id, string displayName, TattooPlacement placement, string pngPath, float price = 0f, string source = "API")
		{
			return TattooRegistry.Add(new TattooDef
			{
				Id = id,
				DisplayName = (string.IsNullOrWhiteSpace(displayName) ? id : displayName),
				Placement = placement,
				Price = ((price < 0f) ? 0f : price),
				PngPath = pngPath,
				Source = (string.IsNullOrWhiteSpace(source) ? "API" : source)
			});
		}

		[MethodImpl(MethodImplOptions.NoInlining)]
		public static bool RegisterTattooFromResource(string id, string displayName, TattooPlacement placement, string resourceName, float price = 0f, string source = "API")
		{
			return RegisterTattooFromResource(id, displayName, placement, Assembly.GetCallingAssembly(), resourceName, price, source);
		}

		public static bool RegisterTattooFromResource(string id, string displayName, TattooPlacement placement, Assembly assembly, string resourceName, float price = 0f, string source = "API")
		{
			return TattooRegistry.Add(new TattooDef
			{
				Id = id,
				DisplayName = (string.IsNullOrWhiteSpace(displayName) ? id : displayName),
				Placement = placement,
				Price = ((price < 0f) ? 0f : price),
				ResourceAssembly = assembly,
				ResourceName = resourceName,
				Source = (string.IsNullOrWhiteSpace(source) ? "API" : source)
			});
		}
	}
	public sealed class Core : MelonMod
	{
		public static Core Instance { get; private set; }

		public static Instance Log { get; private set; }

		public override void OnInitializeMelon()
		{
			Instance = this;
			Log = ((MelonBase)this).LoggerInstance;
			Preferences.Initialize();
			ExamplePack.ExtractIfEnabled();
			int value = TattooRegistry.AddRange(PackLoader.LoadAll());
			try
			{
				((MelonBase)this).HarmonyInstance.PatchAll(Assembly.GetExecutingAssembly());
			}
			catch (Exception ex)
			{
				Log.Warning("Harmony patch failed: " + ex.Message);
			}
			Log.Msg($"Inkorporated 1.1.0 - {value} pack tattoo(s) loaded ({TattooRegistry.AllDefs.Count} total). Shop injection armed.");
			Log.Msg("Drop packs in: " + PackLoader.PacksRoot);
		}

		public override void OnSceneWasUnloaded(int buildIndex, string sceneName)
		{
			ShopInjector.Reset();
		}
	}
}
namespace Inkorporated.Shop
{
	[HarmonyPatch(typeof(CharacterCustomizationCategory), "Awake")]
	internal static class CategoryAwakePatch
	{
		private static void Prefix(CharacterCustomizationCategory __instance)
		{
			ShopInjector.TryInject(__instance);
		}
	}
	internal static class ShopInjector
	{
		private static readonly HashSet<IntPtr> _processed = new HashSet<IntPtr>();

		public static void Reset()
		{
			_processed.Clear();
		}

		public static void TryInject(CharacterCustomizationCategory category)
		{
			try
			{
				if ((Object)(object)category == (Object)null || !_processed.Add(((Il2CppObjectBase)category).Pointer) || (Object)(object)((Component)category).GetComponentInParent<TattooShopUI>() == (Object)null)
				{
					return;
				}
				Il2CppArrayBase<CharacterCustomizationOption> componentsInChildren = ((Component)category).GetComponentsInChildren<CharacterCustomizationOption>(true);
				if (componentsInChildren == null || componentsInChildren.Length == 0)
				{
					return;
				}
				CharacterCustomizationOption val = PickTemplate(componentsInChildren);
				if ((Object)(object)val == (Object)null)
				{
					return;
				}
				List<string> list = new List<string>();
				for (int i = 0; i < componentsInChildren.Length; i++)
				{
					string text = (((Object)(object)componentsInChildren[i] != (Object)null) ? componentsInChildren[i].Label : null);
					if (!string.IsNullOrEmpty(text))
					{
						list.Add(text);
					}
				}
				int num = 0;
				foreach (TattooDef allDef in TattooRegistry.AllDefs)
				{
					if (LabelsContain(list, TattooRegistry.CategoryToken(allDef.Placement)) && TattooRegistry.EnsureRegistered(allDef) && !HasLabel(componentsInChildren, allDef.ResourcePath) && CloneOption(category, val, allDef))
					{
						num++;
					}
				}
				if (num <= 0)
				{
					return;
				}
				Transform parent = ((Component)val).transform.parent;
				if ((Object)(object)parent != (Object)null)
				{
					List<Transform> list2 = new List<Transform>();
					for (int j = 0; j < parent.childCount; j++)
					{
						Transform child = parent.GetChild(j);
						if ((Object)(object)((Component)child).GetComponent<CharacterCustomizationOption>() == (Object)null)
						{
							list2.Add(child);
						}
					}
					foreach (Transform item in list2)
					{
						item.SetAsLastSibling();
					}
				}
				Instance log = Core.Log;
				if (log != null)
				{
					log.Msg($"Injected {num} custom tattoo(s) into category '{category.CategoryName}'.");
				}
			}
			catch (Exception ex)
			{
				Instance log2 = Core.Log;
				if (log2 != null)
				{
					log2.Warning("Shop injection error: " + ex.Message);
				}
			}
		}

		private static CharacterCustomizationOption PickTemplate(Il2CppArrayBase<CharacterCustomizationOption> opts)
		{
			for (int i = 0; i < opts.Length; i++)
			{
				if ((Object)(object)opts[i] != (Object)null && ((Component)opts[i]).gameObject.activeSelf)
				{
					return opts[i];
				}
			}
			if (opts.Length <= 0)
			{
				return null;
			}
			return opts[0];
		}

		private static bool LabelsContain(List<string> labels, string token)
		{
			foreach (string label in labels)
			{
				if (label.IndexOf(token, StringComparison.OrdinalIgnoreCase) >= 0)
				{
					return true;
				}
			}
			return false;
		}

		private static bool HasLabel(Il2CppArrayBase<CharacterCustomizationOption> opts, string label)
		{
			if (label == null)
			{
				return false;
			}
			for (int i = 0; i < opts.Length; i++)
			{
				if ((Object)(object)opts[i] != (Object)null && string.Equals(opts[i].Label, label, StringComparison.Ordinal))
				{
					return true;
				}
			}
			return false;
		}

		private static bool CloneOption(CharacterCustomizationCategory category, CharacterCustomizationOption template, TattooDef def)
		{
			//IL_0025: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				Transform parent = ((Component)template).transform.parent;
				GameObject val = ((Il2CppObjectBase)Object.Instantiate<GameObject>(((Component)template).gameObject, parent, false)).Cast<GameObject>();
				val.transform.localScale = Vector3.one;
				((Object)val).name = "Inkorporated_" + def.Source + "_" + def.Id;
				CharacterCustomizationOption component = val.GetComponent<CharacterCustomizationOption>();
				if ((Object)(object)component == (Object)null)
				{
					Object.Destroy((Object)(object)val);
					return false;
				}
				component.Name = def.DisplayName;
				component.Label = def.ResourcePath;
				component.Price = def.Price;
				component.RequireLevel = false;
				if ((Object)(object)component.NameLabel != (Object)null)
				{
					((TMP_Text)component.NameLabel).text = def.DisplayName;
				}
				if ((Object)(object)component.PriceLabel != (Object)null)
				{
					((TMP_Text)component.PriceLabel).text = ((def.Price > 0f) ? Mathf.RoundToInt(def.Price).ToString() : "Free");
				}
				if (!val.activeSelf)
				{
					val.SetActive(true);
				}
				return true;
			}
			catch (Exception ex)
			{
				Instance log = Core.Log;
				if (log != null)
				{
					log.Warning("Tattoo '" + def.Key + "': failed to create shop button - " + ex.Message);
				}
				return false;
			}
		}
	}
}
namespace Inkorporated.Registration
{
	internal static class TattooRegistry
	{
		private static readonly List<TattooDef> _all = new List<TattooDef>();

		private static readonly HashSet<string> _keys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

		public static IReadOnlyList<TattooDef> AllDefs => _all;

		public static bool Add(TattooDef def)
		{
			if (def == null || string.IsNullOrWhiteSpace(def.Id))
			{
				return false;
			}
			if (!_keys.Add(def.Key))
			{
				return false;
			}
			_all.Add(def);
			return true;
		}

		public static int AddRange(IEnumerable<TattooDef> defs)
		{
			int num = 0;
			if (defs == null)
			{
				return 0;
			}
			foreach (TattooDef def in defs)
			{
				if (Add(def))
				{
					num++;
				}
			}
			return num;
		}

		public static string CategoryToken(TattooPlacement p)
		{
			return p switch
			{
				TattooPlacement.Chest => "/chest/", 
				TattooPlacement.LeftArm => "/leftarm/", 
				TattooPlacement.RightArm => "/rightarm/", 
				TattooPlacement.Face => "/face/", 
				_ => "/chest/", 
			};
		}

		private static string SourceLayer(TattooPlacement p)
		{
			return p switch
			{
				TattooPlacement.Chest => "Avatar/Layers/Tattoos/chest/Chest_Bird", 
				TattooPlacement.LeftArm => "Avatar/Layers/Tattoos/leftarm/LeftArm_Web", 
				TattooPlacement.RightArm => "Avatar/Layers/Tattoos/rightarm/RightArm_Web", 
				TattooPlacement.Face => "Avatar/Layers/Tattoos/face/Face_Teardrop", 
				_ => "Avatar/Layers/Tattoos/chest/Chest_Bird", 
			};
		}

		private static string TargetPath(TattooDef def)
		{
			string text = ((def.Placement == TattooPlacement.Face) ? "Face" : def.Placement.ToString().ToLowerInvariant());
			return "Avatar/Layers/Tattoos/custom/" + text + "/" + Sanitize(def.Source) + "_" + Sanitize(def.Id);
		}

		public static bool EnsureRegistered(TattooDef def)
		{
			if (def == null)
			{
				return false;
			}
			if (def.ResourcePath != null)
			{
				return true;
			}
			try
			{
				Texture2D val = def.Texture;
				if ((Object)(object)val == (Object)null)
				{
					if (!string.IsNullOrEmpty(def.PngPath))
					{
						val = TextureUtils.LoadTextureFromFile(def.PngPath, (FilterMode)1, (TextureWrapMode)1);
					}
					else
					{
						if (!(def.ResourceAssembly != null) || string.IsNullOrEmpty(def.ResourceName))
						{
							Instance log = Core.Log;
							if (log != null)
							{
								log.Warning("Tattoo '" + def.Key + "': no texture, PNG path or embedded resource.");
							}
							return false;
						}
						byte[] array = ReadResource(def.ResourceAssembly, def.ResourceName);
						if (array != null)
						{
							val = TextureUtils.LoadTextureFromBytes(array, (FilterMode)1, (TextureWrapMode)1);
						}
					}
					if ((Object)(object)val == (Object)null)
					{
						Instance log2 = Core.Log;
						if (log2 != null)
						{
							log2.Warning("Tattoo '" + def.Key + "': failed to load image.");
						}
						return false;
					}
				}
				string text = TargetPath(def);
				string text2 = SourceLayer(def.Placement);
				if (!AvatarLayerFactory.CreateAndRegisterAvatarLayer(text2, text, def.DisplayName ?? def.Id, val))
				{
					Instance log3 = Core.Log;
					if (log3 != null)
					{
						log3.Warning($"Tattoo '{def.Key}': CreateAndRegisterAvatarLayer failed (source '{text2}').");
					}
					return false;
				}
				def.ResourcePath = text;
				Instance log4 = Core.Log;
				if (log4 != null)
				{
					log4.Msg("Registered tattoo '" + def.Key + "' -> " + text);
				}
				return true;
			}
			catch (Exception ex)
			{
				Instance log5 = Core.Log;
				if (log5 != null)
				{
					log5.Warning("Tattoo '" + def.Key + "': registration error - " + ex.Message);
				}
				return false;
			}
		}

		private static byte[] ReadResource(Assembly asm, string name)
		{
			try
			{
				string name2 = name;
				if (asm.GetManifestResourceStream(name2) == null)
				{
					string[] manifestResourceNames = asm.GetManifestResourceNames();
					foreach (string text in manifestResourceNames)
					{
						if (text == name || text.EndsWith("." + name, StringComparison.OrdinalIgnoreCase))
						{
							name2 = text;
							break;
						}
					}
				}
				using Stream stream = asm.GetManifestResourceStream(name2);
				if (stream == null)
				{
					Instance log = Core.Log;
					if (log != null)
					{
						log.Warning("Embedded resource not found: '" + name + "' in " + asm.GetName().Name);
					}
					return null;
				}
				using MemoryStream memoryStream = new MemoryStream();
				stream.CopyTo(memoryStream);
				return memoryStream.ToArray();
			}
			catch (Exception ex)
			{
				Instance log2 = Core.Log;
				if (log2 != null)
				{
					log2.Warning("Embedded resource read failed '" + name + "': " + ex.Message);
				}
				return null;
			}
		}

		private static string Sanitize(string s)
		{
			if (string.IsNullOrEmpty(s))
			{
				return "x";
			}
			StringBuilder stringBuilder = new StringBuilder(s.Length);
			foreach (char c in s)
			{
				stringBuilder.Append((char.IsLetterOrDigit(c) || c == '-' || c == '_') ? c : '_');
			}
			return stringBuilder.ToString();
		}
	}
}
namespace Inkorporated.Model
{
	public enum TattooPlacement
	{
		Chest,
		LeftArm,
		RightArm,
		Face
	}
	public sealed class TattooDef
	{
		public string Id;

		public string DisplayName;

		public TattooPlacement Placement;

		public float Price;

		public string PngPath;

		public Texture2D Texture;

		public Assembly ResourceAssembly;

		public string ResourceName;

		public string Source;

		public string ResourcePath;

		public string Key => (Source ?? "?") + "/" + (Id ?? "?");
	}
}
namespace Inkorporated.Content
{
	internal static class ExamplePack
	{
		private const string ResourcePrefix = "Inkorporated.Assets.ExamplePack.";

		public static void ExtractIfEnabled()
		{
			if (!Preferences.LoadExamplePack)
			{
				return;
			}
			string text = Path.Combine(PackLoader.PacksRoot, "Examples");
			try
			{
				if (Directory.Exists(text) && File.Exists(Path.Combine(text, "manifest.json")))
				{
					Instance log = Core.Log;
					if (log != null)
					{
						log.Msg("Example pack already present - leaving it untouched.");
					}
					return;
				}
				Directory.CreateDirectory(text);
				Assembly executingAssembly = Assembly.GetExecutingAssembly();
				int num = 0;
				string[] manifestResourceNames = executingAssembly.GetManifestResourceNames();
				foreach (string text2 in manifestResourceNames)
				{
					if (!text2.StartsWith("Inkorporated.Assets.ExamplePack.", StringComparison.Ordinal))
					{
						continue;
					}
					string path = text2.Substring("Inkorporated.Assets.ExamplePack.".Length);
					using Stream stream = executingAssembly.GetManifestResourceStream(text2);
					if (stream != null)
					{
						using FileStream destination = File.Create(Path.Combine(text, path));
						stream.CopyTo(destination);
						num++;
					}
				}
				Instance log2 = Core.Log;
				if (log2 != null)
				{
					log2.Msg($"Extracted bundled example pack ({num} file(s)) -> {text}");
				}
			}
			catch (Exception ex)
			{
				Instance log3 = Core.Log;
				if (log3 != null)
				{
					log3.Warning("Example pack extraction failed: " + ex.Message);
				}
			}
		}
	}
	internal static class PackLoader
	{
		public static string PacksRoot => Path.Combine(MelonEnvironment.UserDataDirectory, "Inkorporated", "Packs");

		public static List<TattooDef> LoadAll()
		{
			List<TattooDef> list = new List<TattooDef>();
			string packsRoot = PacksRoot;
			try
			{
				Directory.CreateDirectory(packsRoot);
				WriteReadmeIfMissing(packsRoot);
			}
			catch (Exception ex)
			{
				Instance log = Core.Log;
				if (log != null)
				{
					log.Warning("Could not prepare packs folder '" + packsRoot + "': " + ex.Message);
				}
				return list;
			}
			string[] directories = Directory.GetDirectories(packsRoot);
			foreach (string text in directories)
			{
				string path = Path.Combine(text, "manifest.json");
				if (!File.Exists(path))
				{
					continue;
				}
				string name = new DirectoryInfo(text).Name;
				try
				{
					PackManifest packManifest = JsonConvert.DeserializeObject<PackManifest>(File.ReadAllText(path));
					if (packManifest?.tattoos == null)
					{
						Instance log2 = Core.Log;
						if (log2 != null)
						{
							log2.Warning("Pack '" + name + "': manifest has no 'tattoos' array - skipped.");
						}
						continue;
					}
					int num = 0;
					foreach (ManifestEntry tattoo in packManifest.tattoos)
					{
						TattooDef tattooDef = ToDef(name, text, tattoo);
						if (tattooDef != null)
						{
							list.Add(tattooDef);
							num++;
						}
					}
					Instance log3 = Core.Log;
					if (log3 != null)
					{
						log3.Msg($"Pack '{name}' ({packManifest.name ?? "unnamed"}): {num} tattoo(s).");
					}
				}
				catch (Exception ex2)
				{
					Instance log4 = Core.Log;
					if (log4 != null)
					{
						log4.Warning("Pack '" + name + "': failed to read manifest.json - " + ex2.Message);
					}
				}
			}
			return list;
		}

		private static TattooDef ToDef(string packName, string packDir, ManifestEntry e)
		{
			if (e == null || string.IsNullOrWhiteSpace(e.id))
			{
				Instance log = Core.Log;
				if (log != null)
				{
					log.Warning("Pack '" + packName + "': an entry is missing 'id' - skipped.");
				}
				return null;
			}
			if (!TryParsePlacement(e.placement, out var placement))
			{
				Instance log2 = Core.Log;
				if (log2 != null)
				{
					log2.Warning($"Pack '{packName}' tattoo '{e.id}': unknown placement '{e.placement}' (expected chest|leftarm|rightarm|face) - skipped.");
				}
				return null;
			}
			string path = (string.IsNullOrWhiteSpace(e.file) ? (e.id + ".png") : e.file);
			string text = Path.Combine(packDir, path);
			if (!File.Exists(text))
			{
				Instance log3 = Core.Log;
				if (log3 != null)
				{
					log3.Warning($"Pack '{packName}' tattoo '{e.id}': PNG not found at '{text}' - skipped.");
				}
				return null;
			}
			return new TattooDef
			{
				Id = e.id,
				DisplayName = (string.IsNullOrWhiteSpace(e.name) ? e.id : e.name),
				Placement = placement,
				Price = ((e.price < 0f) ? 0f : e.price),
				PngPath = text,
				Source = packName
			};
		}

		private static bool TryParsePlacement(string s, out TattooPlacement placement)
		{
			placement = TattooPlacement.Chest;
			if (string.IsNullOrWhiteSpace(s))
			{
				return false;
			}
			switch (s.Trim().ToLowerInvariant())
			{
			case "chest":
				placement = TattooPlacement.Chest;
				return true;
			case "left_arm":
			case "left":
			case "leftarm":
				placement = TattooPlacement.LeftArm;
				return true;
			case "right":
			case "rightarm":
			case "right_arm":
				placement = TattooPlacement.RightArm;
				return true;
			case "face":
				placement = TattooPlacement.Face;
				return true;
			default:
				return false;
			}
		}

		private static void WriteReadmeIfMissing(string root)
		{
			string path = Path.Combine(root, "README.txt");
			if (!File.Exists(path))
			{
				File.WriteAllText(path, "Inkorporated - custom tattoo packs\n==================================\n\nDrop one folder per pack in this directory. Each pack needs a manifest.json and the PNG files it lists.\n\nFolder layout:\n  Packs/\n    MyPack/\n      manifest.json\n      my_chest_tattoo.png\n      my_face_tattoo.png\n\nmanifest.json:\n{\n  \"name\": \"My Pack\",\n  \"author\": \"you\",\n  \"tattoos\": [\n    { \"id\": \"skull\",   \"name\": \"Skull\",      \"placement\": \"chest\",    \"file\": \"my_chest_tattoo.png\" },\n    { \"id\": \"teardrop\",\"name\": \"Teardrop X\", \"placement\": \"face\",     \"file\": \"my_face_tattoo.png\", \"price\": 250 }\n  ]\n}\n\nplacement: chest | leftarm | rightarm | face\nprice:     optional, omit or 0 for \"Free\"\n\nTip: PNGs must match the game's body/face UV layout to sit correctly on the skin. Use an existing\nin-game tattoo texture as a template. Tattoos appear in the in-game tattoo shop.\n");
			}
		}
	}
	public sealed class PackManifest
	{
		public string name;

		public string author;

		public List<ManifestEntry> tattoos;
	}
	public sealed class ManifestEntry
	{
		public string id;

		public string name;

		public string placement;

		public string file;

		public float price;
	}
}
namespace Inkorporated.Config
{
	internal static class Preferences
	{
		private const string CategoryId = "Inkorporated_01_Main";

		private static MelonPreferences_Category _category;

		private static MelonPreferences_Entry<bool> _loadExamplePack;

		internal static bool LoadExamplePack => _loadExamplePack?.Value ?? false;

		internal static void Initialize()
		{
			if (_category == null)
			{
				_category = MelonPreferences.CreateCategory("Inkorporated_01_Main", "Inkorporated (Custom Tattoos)");
				_loadExamplePack = _category.CreateEntry<bool>("LoadExamplePack", false, "Load example tattoo pack", "OFF by default. When ON, Inkorporated drops a small bundled example pack into UserData/Inkorporated/Packs/Examples on startup (if not already there) so you get a few ready-made tattoos plus a working folder/manifest template to copy for your own pack. Requires a game restart.", false, false, (ValueValidator)null, (string)null);
			}
		}
	}
}