using System; using System.Buffers.Binary; 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.Cryptography; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using UnityEngine; using UnityEngine.SceneManagement; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: IgnoresAccessChecksTo("")] [assembly: AssemblyCompany("tsharp42")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+69530554098175ea97fd1d77707bf3e913105835")] [assembly: AssemblyProduct("PaintingReplacer")] [assembly: AssemblyTitle("PaintingReplacer")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.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.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [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 PaintingReplacer { internal static class ConfigLoader { private static string DefaultSearchDirectory = "PaintingsConfig"; public static string[] GetAllConfigFiles() { string[] configPaths = GetConfigPaths(); List list = new List(); string[] array = configPaths; foreach (string path in array) { string[] collection = (from file in Directory.EnumerateFiles(path) where file.Contains(".paintings.json") select file).ToArray(); list.AddRange(collection); } return list.ToArray(); } private static string[] GetConfigPaths() { string bepInExRootPath = Paths.BepInExRootPath; string path = Path.Combine(Paths.BepInExRootPath); List list = new List(); list.AddRange(Directory.GetDirectories(path, DefaultSearchDirectory, SearchOption.AllDirectories)); return list.ToArray(); } } public static class ImageDimensions { public static (int Width, int Height) GetDimensions(string path) { using FileStream fileStream = File.OpenRead(path); Span buffer = stackalloc byte[8]; fileStream.Read(buffer); if (buffer[0] == 137 && buffer[1] == 80 && buffer[2] == 78 && buffer[3] == 71) { return GetPngDimensions(fileStream); } if (buffer[0] == byte.MaxValue && buffer[1] == 216) { fileStream.Position = 0L; return GetJpegDimensions(fileStream); } if (buffer[0] == 66 && buffer[1] == 77) { fileStream.Position = 0L; return GetBmpDimensions(fileStream); } if (buffer[0] == 82 && buffer[1] == 73 && buffer[2] == 70 && buffer[3] == 70) { fileStream.Position = 8L; Span buffer2 = stackalloc byte[4]; fileStream.Read(buffer2); if (buffer2[0] == 87 && buffer2[1] == 69 && buffer2[2] == 66 && buffer2[3] == 80) { fileStream.Position = 0L; return GetWebpDimensions(fileStream); } } return (Width: -1, Height: -1); } private static (int Width, int Height) GetPngDimensions(Stream s) { s.Position = 16L; Span buffer = stackalloc byte[8]; s.Read(buffer); int item = (int)BinaryPrimitives.ReadUInt32BigEndian(buffer.Slice(0, 4)); int item2 = (int)BinaryPrimitives.ReadUInt32BigEndian(buffer.Slice(4, 4)); return (Width: item, Height: item2); } private static (int Width, int Height) GetBmpDimensions(Stream s) { s.Position = 18L; Span buffer = stackalloc byte[8]; s.Read(buffer); int item = BitConverter.ToInt32(buffer.Slice(0, 4)); int item2 = BitConverter.ToInt32(buffer.Slice(4, 4)); return (Width: item, Height: item2); } private static (int Width, int Height) GetJpegDimensions(Stream s) { using BinaryReader binaryReader = new BinaryReader(s); binaryReader.ReadUInt16(); while (s.Position < s.Length) { if (binaryReader.ReadByte() != byte.MaxValue) { continue; } byte b = binaryReader.ReadByte(); while (true) { switch (b) { case byte.MaxValue: goto IL_002c; default: { ushort num = ReadBigEndianUInt16(binaryReader); if ((b >= 192 && b <= 195) || (b >= 197 && b <= 199) || (b >= 201 && b <= 203) || (b >= 205 && b <= 207)) { binaryReader.ReadByte(); int item = ReadBigEndianUInt16(binaryReader); int item2 = ReadBigEndianUInt16(binaryReader); return (Width: item2, Height: item); } s.Position += num - 2; break; } case 216: case 217: break; } break; IL_002c: b = binaryReader.ReadByte(); } } throw new InvalidDataException("Could not find JPEG dimensions."); } private static ushort ReadBigEndianUInt16(BinaryReader br) { int num = br.ReadByte(); int num2 = br.ReadByte(); return (ushort)((num << 8) | num2); } private static (int Width, int Height) GetWebpDimensions(Stream s) { using BinaryReader binaryReader = new BinaryReader(s); s.Position = 12L; string text = new string(binaryReader.ReadChars(4)); uint num = binaryReader.ReadUInt32(); switch (text) { case "VP8 ": { s.Position += 10L; ushort num5 = binaryReader.ReadUInt16(); ushort num6 = binaryReader.ReadUInt16(); return (Width: num5 & 0x3FFF, Height: num6 & 0x3FFF); } case "VP8L": { binaryReader.ReadByte(); uint num4 = (uint)(binaryReader.ReadByte() | (binaryReader.ReadByte() << 8) | (binaryReader.ReadByte() << 16) | (binaryReader.ReadByte() << 24)); int item = (int)((num4 & 0x3FFF) + 1); int item2 = (int)(((num4 >> 14) & 0x3FFF) + 1); return (Width: item, Height: item2); } case "VP8X": { s.Position += 4L; int num2 = binaryReader.ReadByte() | (binaryReader.ReadByte() << 8) | (binaryReader.ReadByte() << 16); int num3 = binaryReader.ReadByte() | (binaryReader.ReadByte() << 8) | (binaryReader.ReadByte() << 16); return (Width: num2 + 1, Height: num3 + 1); } default: return (Width: -1, Height: -1); } } } internal class ImageFile { public enum ImageAspectRatioType : byte { Landscape, Portrait, Square } private string? _fileName; private string? _fileNameHash; private float _ratio; private int _width; private int _height; public string? FileNameHash => _fileNameHash; public string? FullPath { get { return _fileName; } set { _fileName = value; if (!File.Exists(value)) { _fileName = null; } if (value != null) { _fileNameHash = ImageFileHelper.GetImageFileHash(RelativePath ?? ""); var (num, num2) = ImageDimensions.GetDimensions(value); if (num > 0 && num2 > 0) { _width = num; _height = num2; _ratio = (float)num / (float)num2; } } } } public string? RelativePath { get { if (_fileName != null) { return _fileName.Replace(Paths.BepInExRootPath, ""); } return null; } } public float Ratio => _ratio; public int Width => _width; public int Height => _height; public bool IsValid { get { if (_fileName == null) { return false; } if (_fileNameHash == null) { return false; } if (_width == -1 || _height == -1) { return false; } return true; } } public ImageAspectRatioType AspectRatioType { get { if (_ratio > 1.3f) { return ImageAspectRatioType.Landscape; } if (_ratio < 0.85714287f) { return ImageAspectRatioType.Portrait; } return ImageAspectRatioType.Square; } } } internal static class ImageFileHelper { private static string[] Extensions = new string[4] { ".png", ".jpg", ".jpeg", ".bmp" }; private static string[] DefaultSearchDirectories = new string[1] { "CustomPaintings" }; private static MD5 MD5Hasher = MD5.Create(); public static string[] GetImagePaths(string[]? imageDirectories = null) { if (imageDirectories == null) { imageDirectories = DefaultSearchDirectories; } string bepInExRootPath = Paths.BepInExRootPath; string path = Path.Combine(Paths.BepInExRootPath); List list = new List(); string[] array = imageDirectories; foreach (string searchPattern in array) { list.AddRange(Directory.GetDirectories(path, searchPattern, SearchOption.AllDirectories)); } return list.ToArray(); } public static ImageFile[] GetImageFiles(string[] ImagePaths) { List list = new List(); List list2 = new List(); foreach (string path in ImagePaths) { string[] array = (from file in Directory.EnumerateFiles(path) where Extensions.Contains(Path.GetExtension(file).ToLower()) select file).ToArray(); string[] array2 = array; foreach (string fullPath in array2) { ImageFile imageFile = new ImageFile { FullPath = fullPath }; if (imageFile.IsValid) { list.Add(new ImageFile { FullPath = fullPath }); } } } list.OrderBy((ImageFile f) => f.FileNameHash); return list.ToArray(); } public static string GetRelativePath(string path) { return path.Replace(Paths.BepInExRootPath, ""); } public static string GetImageFileHash(string path) { string s = Path.GetFileName(path).ToLowerInvariant(); byte[] array = MD5Hasher.ComputeHash(Encoding.UTF8.GetBytes(s)); StringBuilder stringBuilder = new StringBuilder(); byte[] array2 = array; foreach (byte b in array2) { stringBuilder.Append(b.ToString("X2")); } return stringBuilder.ToString(); } public static ImageFile? GetReplacementImage(ImageFile[] images, Vector3 position, ImageFile.ImageAspectRatioType aspectRatioType, string targetName, ManualLogSource logger) { //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: Unknown result type (might be due to invalid IL or missing references) List list = (from x in images where x.AspectRatioType == aspectRatioType orderby x.FileNameHash select x).ToList(); if (list.Count == 0) { logger.LogWarning((object)$"No replacement images found for {aspectRatioType}"); return null; } string text = $"{Mathf.RoundToInt(position.x * 10f)},{Mathf.RoundToInt(position.y * 10f)},{Mathf.RoundToInt(position.z * 10f)}:{targetName}:{aspectRatioType}"; byte[] value = MD5Hasher.ComputeHash(Encoding.UTF8.GetBytes(text)); int num = BitConverter.ToInt32(value, 0) & 0x7FFFFFFF; ImageFile imageFile = list[num % list.Count]; logger.LogInfo((object)$"-- Image Selection Input: {num} = {text}"); logger.LogInfo((object)("-- Swap: " + targetName + " -> [" + imageFile.FileNameHash + "]")); return imageFile; } } [BepInPlugin("tsharp42.PaintingReplacer", "PaintingReplacer", "0.0.2")] public class PaintingReplacer : BaseUnityPlugin { [HarmonyPatch(typeof(PlayerAvatar), "LoadingLevelAnimationCompletedRPC")] public class PaintingSwapPatch { private static void Postfix() { doSwap = true; } } private static ImageFile[] replacementImages = new ImageFile[0]; private static bool doSwap = false; private static Dictionary paintingReferences = new Dictionary(); private static List loadedTextures = new List(); private static List loadedMaterials = new List(); internal static PaintingReplacer Instance { get; private set; } = null; internal static ManualLogSource Logger => Instance._logger; private ManualLogSource _logger => ((BaseUnityPlugin)this).Logger; internal Harmony? Harmony { get; set; } private void Awake() { Instance = this; ((Component)this).gameObject.transform.parent = null; ((Object)((Component)this).gameObject).hideFlags = (HideFlags)61; Patch(); Logger.LogInfo((object)$"{((BaseUnityPlugin)this).Info.Metadata.GUID} v{((BaseUnityPlugin)this).Info.Metadata.Version} Begin initialisation"); string[] imagePaths = ImageFileHelper.GetImagePaths(); Logger.LogInfo((object)$"{imagePaths.Length} image paths found, Loading Images..."); Stopwatch stopwatch = Stopwatch.StartNew(); replacementImages = ImageFileHelper.GetImageFiles(imagePaths); Logger.LogInfo((object)$"{replacementImages.Length} file paths found"); ImageFile[] array = replacementImages; foreach (ImageFile imageFile in array) { Logger.LogDebug((object)("Image File: \nPath : " + imageFile.RelativePath + "\nHash : " + imageFile.FileNameHash + "\nRatio: " + imageFile.Ratio + "\nClassification: " + imageFile.AspectRatioType.ToString() + "\nSize : " + imageFile.Width + " x " + imageFile.Height)); } stopwatch.Stop(); Logger.LogInfo((object)$"Loaded {replacementImages.Length} images in {stopwatch.Elapsed.TotalSeconds}s"); string[] allConfigFiles = ConfigLoader.GetAllConfigFiles(); Logger.LogInfo((object)$"Config files found: {allConfigFiles.Length}"); string[] array2 = allConfigFiles; foreach (string text in array2) { Logger.LogInfo((object)("Loading Painting Config: " + text.Replace(Paths.BepInExRootPath, ""))); if (!File.Exists(text)) { continue; } string text2 = File.ReadAllText(text); Dictionary dictionary = JsonConvert.DeserializeObject>(text2); if (dictionary == null) { continue; } Logger.LogInfo((object)$"Replacements in this config: {dictionary.Count}"); foreach (KeyValuePair item in dictionary) { Logger.LogDebug((object)$"{item.Key}:{item.Value}"); if (!paintingReferences.TryAdd(item.Key, item.Value)) { Logger.LogWarning((object)("Duplicate painting config: " + item.Key + " in " + text)); paintingReferences[item.Key] = item.Value; } } } } internal void Patch() { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Expected O, but got Unknown //IL_0025: Expected O, but got Unknown if (Harmony == null) { Harmony val = new Harmony(((BaseUnityPlugin)this).Info.Metadata.GUID); Harmony val2 = val; Harmony = val; } Harmony.PatchAll(); } internal void Unpatch() { Harmony? harmony = Harmony; if (harmony != null) { harmony.UnpatchSelf(); } } private void Update() { //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00dc: Unknown result type (might be due to invalid IL or missing references) //IL_0229: Unknown result type (might be due to invalid IL or missing references) //IL_022e: Unknown result type (might be due to invalid IL or missing references) //IL_0235: Unknown result type (might be due to invalid IL or missing references) //IL_0263: Unknown result type (might be due to invalid IL or missing references) //IL_026a: Expected O, but got Unknown //IL_02aa: Unknown result type (might be due to invalid IL or missing references) //IL_02b1: Expected O, but got Unknown if (!doSwap) { return; } doSwap = false; Logger.LogDebug((object)$"Destroying previously allocated textures: {loadedTextures.Count}"); foreach (Texture2D loadedTexture in loadedTextures) { Object.Destroy((Object)(object)loadedTexture); } loadedTextures.Clear(); Logger.LogDebug((object)$"Destroying previously allocated materials: {loadedMaterials.Count}"); foreach (Material loadedMaterial in loadedMaterials) { Object.Destroy((Object)(object)loadedMaterial); } loadedMaterials.Clear(); int num = 0; Scene activeScene = SceneManager.GetActiveScene(); GameObject val = ((IEnumerable)((Scene)(ref activeScene)).GetRootGameObjects()).FirstOrDefault((Func)((GameObject g) => ((Object)g).name == "Level Generator")); if ((Object)(object)val == (Object)null) { Logger.LogError((object)"Could not find 'Level Generator'"); return; } MeshRenderer[] componentsInChildren = val.GetComponentsInChildren(); foreach (MeshRenderer val2 in componentsInChildren) { Material[] sharedMaterials = ((Renderer)val2).sharedMaterials; bool flag = false; for (int num3 = 0; num3 < sharedMaterials.Length; num3++) { if (!Object.op_Implicit((Object)(object)sharedMaterials[num3])) { Logger.LogDebug((object)"No material"); continue; } string text = ((Object)sharedMaterials[num3]).name.Trim(); if (paintingReferences.TryGetValue(text, out var aspectRatioType)) { int num4 = replacementImages.Count((ImageFile im) => im.AspectRatioType == aspectRatioType); Logger.LogDebug((object)$"- {((Object)val2).name} > {text} ({aspectRatioType}) {num4}"); if (num4 == 0) { Logger.LogWarning((object)$"-- Tried to swap on type {aspectRatioType} but there are no images of this type"); continue; } Vector3 position = ((Component)val2).transform.position; ImageFile replacementImage = ImageFileHelper.GetReplacementImage(replacementImages, position, aspectRatioType, text, Logger); if (replacementImage != null) { byte[] array = File.ReadAllBytes(replacementImage.FullPath); Texture2D val3 = new Texture2D(2, 2); if (!ImageConversion.LoadImage(val3, array, true)) { Logger.LogWarning((object)("Failed to load image: " + replacementImage.RelativePath)); Object.Destroy((Object)(object)val3); continue; } ((Texture)val3).filterMode = (FilterMode)1; Material val4 = new Material(sharedMaterials[num3]); val4.mainTexture = (Texture)(object)val3; sharedMaterials[num3] = val4; loadedTextures.Add(val3); loadedMaterials.Add(val4); num++; flag = true; } } else if (text.ToLower().Contains("painting") && !text.ToLower().Contains("frame")) { Logger.LogWarning((object)("No Replacement for " + ((Object)val2).name + " > " + text)); } } if (flag) { ((Renderer)val2).sharedMaterials = sharedMaterials; } } Logger.LogInfo((object)("Total Paintings Swapped: " + num)); } } }