using System; 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 AssetShards; using BepInEx; using BepInEx.Logging; using BepInEx.Unity.IL2CPP; using Clonesoft.Json; using CustomPalettes.Core; using CustomPalettes.Data; using GameData; using HarmonyLib; using Il2CppInterop.Runtime.InteropTypes.Arrays; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyFileVersion("1.2.0")] [assembly: AssemblyInformationalVersion("1.2.0")] [assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")] [assembly: AssemblyVersion("1.2.0.0")] [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; } } } internal class ManifestInfo { internal const string TSName = "CustomPalettes"; internal const string TSDescription = "Add custom palettes to the game using json files and pngs!"; internal const string TSVersion = "1.2.0"; internal const string TSAuthor = "AuriRex"; internal const string TSWebsite = "https://github.com/AuriRex/GTFO_CustomPalettes/"; } namespace CustomPalettes { internal static class L { internal static ManualLogSource Logger { private get; set; } internal static void Info(string msg) { Logger.LogInfo((object)msg); } internal static void Msg(string msg) { Logger.LogMessage((object)msg); } internal static void Debug(string msg) { Logger.LogDebug((object)msg); } internal static void Warning(string msg) { Logger.LogWarning((object)msg); } internal static void Error(string msg) { Logger.LogError((object)msg); } internal static void Exception(Exception ex) { Logger.LogError((object)ex.Message); Logger.LogWarning((object)("StackTrace:\n" + ex.StackTrace)); } } public static class Patches { [HarmonyPatch(typeof(AssetShardManager), "Setup")] internal static class AssetShardManager_Setup_Patch { [HarmonyPriority(600)] public static void Postfix() { Plugin.OnAssetShardManagerReady(); } } [HarmonyPatch(typeof(GameDataInit), "Initialize")] internal static class GameDataInit_Initialize_Patch { [HarmonyPriority(600)] public static void Postfix() { Plugin.OnGameDataInit(); } } } [BepInPlugin("dev.aurirex.gtfo.custompalettes", "Custom Palettes", "1.2.0")] public class Plugin : BasePlugin { public const string GUID = "dev.aurirex.gtfo.custompalettes"; public const string MOD_NAME = "Custom Palettes"; public const string VERSION = "1.2.0"; private static readonly Harmony _harmony = new Harmony("dev.aurirex.gtfo.custompalettes"); private static bool _hasInitedOnce = false; public override void Load() { L.Logger = ((BasePlugin)this).Log; _harmony.PatchAll(Assembly.GetExecutingAssembly()); PaletteManager.Setup(); PaletteManager.LoadPalettes(); } internal static void OnAssetShardManagerReady() { L.Debug("AssetShardManager ready, Injecting Palettes"); TextureLoader.Setup(PaletteManager.Palettes); PaletteManager.InjectPalettes(); } internal static void OnGameDataInit() { L.Debug("GameDataInit.Initialize called."); if (_hasInitedOnce) { L.Warning("Reloading Custom Palettes ..."); PaletteManager.LoadPalettes(); TextureLoader.Setup(PaletteManager.Palettes, doCleanup: true); PaletteManager.InjectPalettes(forceRegeneration: true); PersistentInventoryManager.m_dirty = true; } _hasInitedOnce = true; } } } namespace CustomPalettes.Data { public class CustomPalette { public string Name { get; set; } = "Custom Palette"; public string Author { get; set; } = string.Empty; public string SortingName { get; set; } = "MyCustomPalette"; public uint VanityGroupID { get; set; } public PaletteData Data { get; set; } = new PaletteData(); [JsonIgnore] public string FileName { get; internal set; } = string.Empty; } public class PaletteData { public class Tone { public string HexColor { get; set; } = "#FFFFFF"; public string TextureFile { get; set; } = string.Empty; public int MaterialOverride { get; set; } = -1; } [JsonIgnore] private static readonly Tone _errorTone = new Tone { m_color = Color.magenta, m_texture = Texture2D.whiteTexture, m_materialOverride = -1 }; public Tone PrimaryTone { get; set; } = new Tone(); public Tone SecondaryTone { get; set; } = new Tone(); public Tone TertiaryTone { get; set; } = new Tone(); public Tone QuaternaryTone { get; set; } = new Tone(); public Tone QuinaryTone { get; set; } = new Tone(); public float TextureTiling { get; set; } = 1f; [JsonIgnore] public IEnumerable Tones { get { if (PrimaryTone != null) { yield return PrimaryTone; } if (SecondaryTone != null) { yield return SecondaryTone; } if (TertiaryTone != null) { yield return TertiaryTone; } if (QuaternaryTone != null) { yield return QuaternaryTone; } if (QuinaryTone != null) { yield return QuinaryTone; } } } internal Tone GetTone(int v) { return (Tone)(v switch { 2 => GetTone(SecondaryTone), 3 => GetTone(TertiaryTone), 4 => GetTone(QuaternaryTone), 5 => GetTone(QuinaryTone), _ => GetTone(PrimaryTone), }); } private static Tone GetTone(Tone tone) { //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Expected O, but got Unknown if (tone == null) { return _errorTone; } Color color = default(Color); if (!ColorUtility.TryParseHtmlString(tone.HexColor, ref color) && !ColorUtility.TryParseHtmlString("#" + tone.HexColor, ref color)) { return _errorTone; } Texture2D texture = TextureLoader.GetTexture(tone.TextureFile); return new Tone { m_color = color, m_texture = texture, m_materialOverride = tone.MaterialOverride }; } } } namespace CustomPalettes.Core { public static class PaletteManager { private static string _path; private static readonly Dictionary _nameToPalette = new Dictionary(); private static readonly List _palettes = new List(); private static readonly JsonSerializerSettings _jsonSettings = new JsonSerializerSettings { Formatting = (Formatting)1 }; private const string TEMPLATE_FILE_NAME = "_template_palette.json"; public static string CustomPalettesPath => _path ?? (_path = Path.Combine(Paths.BepInExRootPath, "Assets", "CustomPalettes")); public static string BLOCK_PREFIX => "CustomPalette_".ToUpper(); public static bool DoLoadTemplateFile { get; set; } = false; public static IEnumerable Palettes => _palettes; public static bool TryGetPaletteFromBlock(VanityItemsTemplateDataBlock block, out CustomPalette palette) { return TryGetPaletteFromId(((GameDataBlockBase)(object)block).name, out palette); } public static bool TryGetPaletteFromId(string identifier, out CustomPalette palette) { return _nameToPalette.TryGetValue(identifier, out palette); } internal static void Setup() { if (!Directory.Exists(CustomPalettesPath)) { Directory.CreateDirectory(CustomPalettesPath); } string path = Path.Combine(CustomPalettesPath, "_template_palette.json"); if (!File.Exists(path)) { string contents = JsonConvert.SerializeObject((object)GetTemplate(), _jsonSettings); File.WriteAllText(path, contents); } } internal static void LoadPalettes() { _palettes.Clear(); L.Info("Loading Custom Palettes from [" + CustomPalettesPath + "]!"); string[] files = Directory.GetFiles(CustomPalettesPath); foreach (string path in files) { string fileName = Path.GetFileName(path); if (Path.HasExtension(path) && !(Path.GetExtension(path) != ".json") && (!(fileName == "_template_palette.json") || DoLoadTemplateFile)) { try { CustomPalette customPalette = JsonConvert.DeserializeObject(File.ReadAllText(path), _jsonSettings); L.Info($"Loaded Custom Palette: ({customPalette.SortingName}): {customPalette.Author} | {customPalette.Name}"); customPalette.FileName = fileName; Sanitize(customPalette); _palettes.Add(customPalette); } catch (Exception ex) { L.Exception(ex); } } } } internal static void Sanitize(CustomPalette pal) { IEnumerable enumerable = pal?.Data?.Tones; if (enumerable == null) { return; } foreach (PaletteData.Tone item in enumerable) { item.TextureFile = SanitizePath(item.TextureFile); } } internal static string SanitizePath(string tex) { char[] invalidPathChars = Path.GetInvalidPathChars(); foreach (char oldChar in invalidPathChars) { tex = tex.Replace(oldChar, '_'); } while (tex.Contains("..")) { tex = tex.Replace("..", ""); } return tex.Replace(":", ""); } private static CustomPalette GetTemplate() { return new CustomPalette { Author = "YourNameHere", Name = "Template Palette", SortingName = "UsedToSort", Data = new PaletteData { PrimaryTone = new PaletteData.Tone { HexColor = "#FF0000" }, SecondaryTone = new PaletteData.Tone { HexColor = "#00FF00" }, TertiaryTone = new PaletteData.Tone { HexColor = "#0000FF" }, QuaternaryTone = new PaletteData.Tone { HexColor = "#FFFF00" }, QuinaryTone = new PaletteData.Tone { HexColor = "#00FFFF" } } }; } private static bool TryGetBlock(string name, out VanityItemsTemplateDataBlock block) { block = GameDataBlockBase.GetBlock(GameDataBlockBase.GetBlockID(name)); return block != null; } internal static void InjectPalettes(bool forceRegeneration = false) { //IL_0180: Unknown result type (might be due to invalid IL or missing references) //IL_0187: Expected O, but got Unknown L.Info($"Injecting {_palettes.Count} Custom Palettes ..."); IOrderedEnumerable orderedEnumerable = _palettes.OrderBy((CustomPalette pal) => $"{pal.Author}_{pal.SortingName}_{pal.FileName}"); _nameToPalette.Clear(); foreach (VanityItemsTemplateDataBlock allBlock in GameDataBlockBase.GetAllBlocks()) { if (((GameDataBlockBase)(object)allBlock).name.StartsWith(BLOCK_PREFIX)) { ((GameDataBlockBase)(object)allBlock).internalEnabled = false; } } foreach (CustomPalette item in orderedEnumerable) { try { string text = BLOCK_PREFIX + item.FileName.ToUpper(); if (TryGetBlock(text, out var block)) { L.Info($"Found existing block for \"{text}\". ({((GameDataBlockBase)(object)block).persistentID})"); block.prefab = GeneratePrefab(text, item.Data, forceRegeneration); block.publicName = item.Name; ((GameDataBlockBase)(object)block).internalEnabled = true; _nameToPalette[text] = item; continue; } block = new VanityItemsTemplateDataBlock(); ((GameDataBlockBase)(object)block).name = text; ((GameDataBlockBase)(object)block).internalEnabled = true; block.DropWeight = 1f; block.type = (ClothesType)4; block.publicName = item.Name; block.prefab = GeneratePrefab(text, item.Data, forceRegeneration); _nameToPalette[text] = item; GameDataBlockBase.AddBlock(block, -1); L.Info($"Injected new palette \"{text}\". ({((GameDataBlockBase)(object)block).persistentID})"); uint num = item.VanityGroupID; if (num == 0) { num = 36u; } if (!GameDataBlockBase.HasBlock(num)) { L.Warning($"Tried to add Palette \"{item?.Name ?? "Unnamed"}\" into group with ID {num} that doesn't exist!"); } else { GameDataBlockBase.GetBlock(num).Items.Add(((GameDataBlockBase)(object)block).persistentID); L.Info($"Added Palette \"{item?.Name ?? "Unnamed"}\" into group with ID {num}."); } } catch (Exception ex) { L.Warning($"Failed to load Custom Palette \"{item?.Name ?? "Unnamed"}\" ({item.FileName})."); L.Exception(ex); } } } private static string GeneratePrefab(string identifier, PaletteData data, bool forceRegeneration = false) { //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Expected O, but got Unknown if (string.IsNullOrWhiteSpace(identifier)) { throw new ArgumentException("Identifier may not be null or whitespace.", "identifier"); } if (data == null) { throw new ArgumentNullException("data"); } if (AssetShardManager.s_loadedAssetsLookup.ContainsKey(identifier)) { if (!forceRegeneration) { return identifier; } L.Debug("Regenerating Palette prefab for \"" + identifier + "\" ..."); Object.Destroy(AssetShardManager.s_loadedAssetsLookup[identifier]); } GameObject val = new GameObject(identifier); ((Object)val).hideFlags = (HideFlags)61; Object.DontDestroyOnLoad((Object)(object)val); ClothesPalette obj = val.AddComponent(); obj.m_textureTiling = data.TextureTiling; obj.m_primaryTone = data.GetTone(1); obj.m_secondaryTone = data.GetTone(2); obj.m_tertiaryTone = data.GetTone(3); obj.m_quaternaryTone = data.GetTone(4); obj.m_quinaryTone = data.GetTone(5); AssetShardManager.s_loadedAssetsLookup[identifier] = (Object)(object)val; return identifier; } } public static class TextureLoader { private static readonly Dictionary _textures = new Dictionary(); public static void Setup(IEnumerable palettes, bool doCleanup = false) { foreach (CustomPalette palette in palettes) { if (palette != null) { ProcessPaletteData(palette); } } if (doCleanup) { CleanupUnusedTextures(palettes); } } private static void CleanupUnusedTextures(IEnumerable palettes) { HashSet hashSet = new HashSet(_textures.Keys); foreach (CustomPalette palette in palettes) { IEnumerable enumerable = palette?.Data?.Tones; if (enumerable == null) { continue; } foreach (PaletteData.Tone item in enumerable) { if (!string.IsNullOrWhiteSpace(item.TextureFile) && hashSet.Contains(item.TextureFile)) { hashSet.Remove(item.TextureFile); } } } if (hashSet.Count == 0) { return; } L.Debug($"Found {hashSet.Count} unused Textures."); foreach (string item2 in hashSet) { if (_textures.TryGetValue(item2, out var value)) { Object.Destroy((Object)(object)value.Texture); _textures.Remove(item2); L.Debug("Unloaded \"" + item2 + "\"!"); } } } private static void ProcessPaletteData(CustomPalette pal) { PaletteData data = pal.Data; if (data == null) { return; } foreach (PaletteData.Tone tone in data.Tones) { string textureFile = tone.TextureFile; if (!string.IsNullOrWhiteSpace(textureFile) && AttemptLoad(textureFile, pal)) { L.Msg("Loaded Texture \"" + textureFile + "\" for Palette " + pal.FileName); } } } private static bool AttemptLoad(string tex, CustomPalette pal) { //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00a2: Expected O, but got Unknown string path = Path.Combine(PaletteManager.CustomPalettesPath, tex); if (!File.Exists(path)) { PrintLoadError("File doesn't exist!", tex, pal); return false; } DateTime lastWriteTimeUtc = File.GetLastWriteTimeUtc(path); if (_textures.TryGetValue(tex, out var value)) { if (lastWriteTimeUtc == value.EditedTime) { return true; } Object.Destroy((Object)(object)value.Texture); _textures.Remove(tex); } string extension = Path.GetExtension(path); if (extension.ToLower() != ".png" && extension.ToLower() != ".jpg") { PrintLoadError("TextureFile is not a valid image file.", tex, pal); return false; } try { Texture2D val = new Texture2D(2, 2); byte[] array = File.ReadAllBytes(path); if (!ImageConversion.LoadImage(val, Il2CppStructArray.op_Implicit(array), false)) { PrintLoadError("Image data can't be loaded!", tex, pal); Object.Destroy((Object)(object)val); return false; } ((Object)val).hideFlags = (HideFlags)61; Object.DontDestroyOnLoad((Object)(object)val); _textures.Add(tex, new TextureData { Texture = val, EditedTime = lastWriteTimeUtc }); return true; } catch (Exception ex) { PrintLoadError(ex.Message, tex, pal); } return false; } private static void PrintLoadError(string message, string tex, CustomPalette pal) { L.Error($"Texture \"{tex}\" could not be loaded for Palette \"{pal?.Name}\" ({pal.FileName}): {message}"); } public static Texture2D GetTexture(string textureFile) { if (string.IsNullOrWhiteSpace(textureFile)) { return Texture2D.whiteTexture; } if (_textures.TryGetValue(textureFile, out var value)) { return value.Texture; } return Texture2D.whiteTexture; } } public class TextureData { public Texture2D Texture { get; internal set; } public DateTime EditedTime { get; internal set; } } }