Decompiled source of CyrillicFonts v1.1.0

Mods/CyrillicFonts.dll

Decompiled 14 hours ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using CyrillicFonts;
using Il2CppInterop.Runtime.InteropTypes;
using Il2CppSystem.Collections.Generic;
using Il2CppTMPro;
using MelonLoader;
using MelonLoader.Preferences;
using MelonLoader.Utils;
using Microsoft.CodeAnalysis;
using SpellBrigade.ModMenu;
using UnityEngine;
using UnityEngine.TextCore;
using UnityEngine.TextCore.LowLevel;
using UnityEngine.UI;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: MelonInfo(typeof(CyrillicFontsMod), "Cyrillic Fonts", "1.1.0", "Relsev", null)]
[assembly: MelonGame("BoltBlasterGames", "TheSpellBrigade")]
[assembly: MelonOptionalDependencies(new string[] { "ModMenu" })]
[assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")]
[assembly: AssemblyCompany("CyrillicFonts")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.1.0.0")]
[assembly: AssemblyInformationalVersion("1.1.0+9688215a22f9c16196fc6e370f4059e77fce9e0c")]
[assembly: AssemblyProduct("CyrillicFonts")]
[assembly: AssemblyTitle("CyrillicFonts")]
[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 CyrillicFonts
{
	internal static class FontFiles
	{
		public enum Role
		{
			Heading,
			Text
		}

		public sealed class Face
		{
			public string Path;

			public Role Role;

			public int Weight;

			public bool Italic;

			public string Style;
		}

		public static readonly List<Face> Faces = new List<Face>();

		public static string Folder => Path.Combine(MelonEnvironment.UserDataDirectory, "CyrillicFonts");

		public static int Extract()
		{
			Directory.CreateDirectory(Folder);
			Assembly assembly = typeof(FontFiles).Assembly;
			string[] manifestResourceNames = assembly.GetManifestResourceNames();
			string[] files = Directory.GetFiles(Folder);
			foreach (string path in files)
			{
				if (Array.IndexOf<string>(manifestResourceNames, Path.GetFileName(path)) < 0)
				{
					try
					{
						File.Delete(path);
					}
					catch
					{
					}
				}
			}
			files = manifestResourceNames;
			foreach (string text in files)
			{
				if (!text.EndsWith(".ttf", StringComparison.OrdinalIgnoreCase) && !text.EndsWith(".otf", StringComparison.OrdinalIgnoreCase))
				{
					continue;
				}
				using Stream stream = assembly.GetManifestResourceStream(text);
				using MemoryStream memoryStream = new MemoryStream();
				stream.CopyTo(memoryStream);
				byte[] array = memoryStream.ToArray();
				string text2 = Path.Combine(Folder, text);
				if (!File.Exists(text2) || new FileInfo(text2).Length != array.Length)
				{
					File.WriteAllBytes(text2, array);
				}
				Role? value = (text.Contains("irishgrover", StringComparison.OrdinalIgnoreCase) ? new Role?(Role.Heading) : (text.Contains("titillium", StringComparison.OrdinalIgnoreCase) ? new Role?(Role.Text) : ((Role?)null)));
				if (!value.HasValue)
				{
					CyrillicFontsMod.Log.Warning(text + ": unknown font, skipped");
					continue;
				}
				(int weight, bool italic) tuple = ReadStyle(array);
				int item = tuple.weight;
				bool item2 = tuple.italic;
				string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(text);
				fileNameWithoutExtension = fileNameWithoutExtension.Substring(fileNameWithoutExtension.LastIndexOf('-') + 1);
				Faces.Add(new Face
				{
					Path = text2,
					Role = value.Value,
					Weight = item,
					Italic = item2,
					Style = fileNameWithoutExtension
				});
				CyrillicFontsMod.Log.Msg($"font {text}: {value}, weight {item}{(item2 ? ", italic" : "")}");
			}
			return Faces.Count;
		}

		private static (int weight, bool italic) ReadStyle(byte[] font)
		{
			try
			{
				int num = ReadU16(font, 4);
				for (int i = 0; i < num; i++)
				{
					int num2 = 12 + i * 16;
					if (font[num2] == 79 && font[num2 + 1] == 83 && font[num2 + 2] == 47 && font[num2 + 3] == 50)
					{
						int num3 = (int)ReadU32(font, num2 + 8);
						return (weight: ReadU16(font, num3 + 4), italic: (ReadU16(font, num3 + 62) & 1) != 0);
					}
				}
			}
			catch
			{
			}
			return (weight: 400, italic: false);
		}

		private static int ReadU16(byte[] b, int at)
		{
			return (b[at] << 8) | b[at + 1];
		}

		private static uint ReadU32(byte[] b, int at)
		{
			return (uint)((b[at] << 24) | (b[at + 1] << 16) | (b[at + 2] << 8) | b[at + 3]);
		}

		public static Face Pick(Role role, int weight, bool italic)
		{
			Face result = null;
			int num = int.MaxValue;
			foreach (Face face in Faces)
			{
				if (face.Role == role)
				{
					int num2 = Math.Abs(face.Weight - weight) + ((face.Italic != italic) ? 50 : 0);
					if (role == Role.Heading && !string.Equals(face.Style, CyrillicFontsMod.HeadingStyleEntry.Value.ToString(), StringComparison.OrdinalIgnoreCase))
					{
						num2 += 1000;
					}
					if (num2 < num)
					{
						result = face;
						num = num2;
					}
				}
			}
			return result;
		}
	}
	internal static class FontPatcher
	{
		private static readonly HashSet<IntPtr> Patched = new HashSet<IntPtr>();

		private static readonly List<(TMP_FontAsset original, TMP_FontAsset cyrillic)> Inserted = new List<(TMP_FontAsset, TMP_FontAsset)>();

		private static readonly Dictionary<string, TMP_FontAsset> Created = new Dictionary<string, TMP_FontAsset>();

		private static bool Describe(string name, out FontFiles.Role role, out int weight, out bool italic)
		{
			role = FontFiles.Role.Text;
			weight = 400;
			italic = false;
			if (name.StartsWith("IrishGrover", StringComparison.OrdinalIgnoreCase))
			{
				role = FontFiles.Role.Heading;
				return true;
			}
			if (!name.StartsWith("TitilliumWeb-", StringComparison.OrdinalIgnoreCase))
			{
				return false;
			}
			string text = name.Substring("TitilliumWeb-".Length).Replace(" SDF", "").Trim();
			italic = text.EndsWith("Italic", StringComparison.OrdinalIgnoreCase);
			if (italic)
			{
				text = text.Substring(0, text.Length - "Italic".Length);
			}
			int num;
			switch (text.ToLowerInvariant())
			{
			case "extralight":
				num = 200;
				break;
			case "light":
				num = 300;
				break;
			case "regular":
			case "":
				num = 400;
				break;
			case "semibold":
				num = 600;
				break;
			case "bold":
				num = 700;
				break;
			case "black":
				num = 900;
				break;
			default:
				num = 400;
				break;
			}
			weight = num;
			return true;
		}

		public static void Scan()
		{
			if (FontFiles.Faces.Count == 0)
			{
				return;
			}
			bool flag = false;
			foreach (TMP_FontAsset item in Resources.FindObjectsOfTypeAll<TMP_FontAsset>())
			{
				if ((Object)(object)item == (Object)null || Patched.Contains(((Il2CppObjectBase)item).Pointer) || !Describe(((Object)item).name, out var role, out var weight, out var italic))
				{
					continue;
				}
				Patched.Add(((Il2CppObjectBase)item).Pointer);
				FontFiles.Face face = FontFiles.Pick(role, weight, italic);
				if (face == null)
				{
					continue;
				}
				TMP_FontAsset val = Create(face, item);
				if (!((Object)(object)val == (Object)null))
				{
					List<TMP_FontAsset> val2 = item.fallbackFontAssetTable;
					if (val2 == null)
					{
						val2 = (item.fallbackFontAssetTable = new List<TMP_FontAsset>());
					}
					val2.Insert(0, val);
					Inserted.Add((item, val));
					flag = true;
					CyrillicFontsMod.Log.Msg(((Object)item).name + " ← " + Path.GetFileName(face.Path));
				}
			}
			if (flag)
			{
				RefreshTexts();
			}
		}

		public static void Reapply()
		{
			for (int i = 0; i < Inserted.Count; i++)
			{
				var (val, val2) = Inserted[i];
				if (!((Object)(object)val == (Object)null) && Describe(((Object)val).name, out var role, out var weight, out var italic))
				{
					FontFiles.Face face = FontFiles.Pick(role, weight, italic);
					TMP_FontAsset val3 = ((face != null) ? Create(face, val) : null);
					if (!((Object)(object)val3 == (Object)null))
					{
						List<TMP_FontAsset> fallbackFontAssetTable = val.fallbackFontAssetTable;
						fallbackFontAssetTable.Remove(val2);
						fallbackFontAssetTable.Insert(0, val3);
						Inserted[i] = (val, val3);
					}
				}
			}
			RefreshTexts();
		}

		private static TMP_FontAsset Create(FontFiles.Face face, TMP_FontAsset original)
		{
			Shader val = (((Object)(object)((TMP_Asset)original).material != (Object)null) ? ((TMP_Asset)original).material.shader : null);
			string key = face.Path + "|" + (((Object)(object)val != (Object)null) ? ((Object)val).name : "");
			if (Created.TryGetValue(key, out var value) && (Object)(object)value != (Object)null)
			{
				return WithScale(value, original, face.Role);
			}
			TMP_FontAsset val2;
			try
			{
				val2 = TMP_FontAsset.CreateFontAsset(face.Path, 0, 90, 9, (GlyphRenderMode)4165, 1024, 1024);
			}
			catch (Exception ex)
			{
				CyrillicFontsMod.Log.Warning(face.Path + ": " + ex.Message);
				return null;
			}
			if ((Object)(object)val2 == (Object)null)
			{
				CyrillicFontsMod.Log.Warning(face.Path + ": TextMeshPro couldn't load the font");
				return null;
			}
			((Object)val2).name = Path.GetFileNameWithoutExtension(face.Path) + " SDF (Cyrillic Fonts)";
			((Object)val2).hideFlags = (HideFlags)32;
			Object.DontDestroyOnLoad((Object)(object)val2);
			if ((Object)(object)val != (Object)null && (Object)(object)((TMP_Asset)val2).material != (Object)null)
			{
				((TMP_Asset)val2).material.shader = val;
			}
			Created[key] = val2;
			return WithScale(val2, original, face.Role);
		}

		private static TMP_FontAsset WithScale(TMP_FontAsset cyrillic, TMP_FontAsset original, FontFiles.Role role)
		{
			try
			{
				FaceInfo faceInfo = ((TMP_Asset)original).faceInfo;
				FaceInfo faceInfo2 = ((TMP_Asset)cyrillic).faceInfo;
				float num = 1f;
				if (faceInfo.pointSize > 0f && faceInfo2.pointSize > 0f && faceInfo.capLine > 0f && faceInfo2.capLine > 0f)
				{
					num = faceInfo.capLine / faceInfo.pointSize / (faceInfo2.capLine / faceInfo2.pointSize) * ((faceInfo.scale > 0f) ? faceInfo.scale : 1f);
				}
				num *= ((role == FontFiles.Role.Heading) ? CyrillicFontsMod.HeadingScaleEntry.Value : CyrillicFontsMod.TextScaleEntry.Value);
				faceInfo2.scale = Mathf.Clamp(num, 0.5f, 2f);
				((TMP_Asset)cyrillic).faceInfo = faceInfo2;
			}
			catch (Exception ex)
			{
				CyrillicFontsMod.Log.Warning("size match: " + ex.Message);
			}
			return cyrillic;
		}

		private static void RefreshTexts()
		{
			foreach (TMP_Text item in Resources.FindObjectsOfTypeAll<TMP_Text>())
			{
				if (!((Object)(object)item == (Object)null) && !((Object)(object)item.font == (Object)null) && Patched.Contains(((Il2CppObjectBase)item.font).Pointer))
				{
					try
					{
						((Graphic)item).SetAllDirty();
					}
					catch
					{
					}
				}
			}
		}
	}
	internal static class MenuPage
	{
		private static readonly string[] Styles = new string[3] { "Гибрид", "Классика (Irish Grover)", "Рисованный" };

		[MethodImpl(MethodImplOptions.NoInlining)]
		public static void Register()
		{
			Menu.AddPage("CyrillicFonts", "Cyrillic Fonts").Choice("Шрифт заголовков", (IList<string>)Styles, (Func<int>)(() => (int)CyrillicFontsMod.HeadingStyleEntry.Value), (Action<int>)delegate(int i)
			{
				Set(CyrillicFontsMod.HeadingStyleEntry, (HeadingStyle)i);
			}, (int?)(int)CyrillicFontsMod.HeadingStyleEntry.DefaultValue).Number("Размер кириллицы в заголовках", 0.5f, 2f, 0.05f, (Func<float>)(() => CyrillicFontsMod.HeadingScaleEntry.Value), (Action<float>)delegate(float v)
			{
				Set(CyrillicFontsMod.HeadingScaleEntry, v);
			}, (Func<float, string>)Percent, (float?)1f)
				.Number("Размер кириллицы в тексте", 0.5f, 2f, 0.05f, (Func<float>)(() => CyrillicFontsMod.TextScaleEntry.Value), (Action<float>)delegate(float v)
				{
					Set(CyrillicFontsMod.TextScaleEntry, v);
				}, (Func<float, string>)Percent, (float?)1f)
				.Note("Гибрид — латинские по форме буквы от Irish Grover, остальные рисованные.\nКлассика — вся кириллица собрана из букв Irish Grover. Рисованный — весь алфавит рисованный.");
		}

		private static string Percent(float v)
		{
			return (v * 100f).ToString("0", CultureInfo.InvariantCulture) + "%";
		}

		private static void Set<T>(MelonPreferences_Entry<T> entry, T value)
		{
			if (!object.Equals(entry.Value, value))
			{
				entry.Value = value;
				((MelonPreferences_Entry)entry).Category.SaveToFile(false);
			}
		}
	}
	public enum HeadingStyle
	{
		Hybrid,
		Classic,
		Drawn
	}
	public class CyrillicFontsMod : MelonMod
	{
		public static Instance Log;

		internal static MelonPreferences_Entry<bool> EnabledEntry;

		internal static MelonPreferences_Entry<float> HeadingScaleEntry;

		internal static MelonPreferences_Entry<float> TextScaleEntry;

		internal static MelonPreferences_Entry<HeadingStyle> HeadingStyleEntry;

		private float _nextScan;

		public override void OnInitializeMelon()
		{
			Log = ((MelonBase)this).LoggerInstance;
			MelonPreferences_Category obj = MelonPreferences.CreateCategory("CyrillicFonts", "Cyrillic Fonts");
			EnabledEntry = obj.CreateEntry<bool>("Enabled", true, "Enabled", "Кириллица шрифтами в стиле игры", false, false, (ValueValidator)null, (string)null);
			HeadingScaleEntry = obj.CreateEntry<float>("HeadingScale", 1f, "Heading Scale", "Размер кириллицы в заголовках и кнопках", false, false, (ValueValidator)(object)new ValueRange<float>(0.5f, 2f), (string)null);
			TextScaleEntry = obj.CreateEntry<float>("TextScale", 1f, "Text Scale", "Размер кириллицы в остальном тексте", false, false, (ValueValidator)(object)new ValueRange<float>(0.5f, 2f), (string)null);
			HeadingStyleEntry = obj.CreateEntry<HeadingStyle>("HeadingStyle", HeadingStyle.Hybrid, "Heading Style", "Кириллица в заголовках: Hybrid — гибрид, Classic — из букв Irish Grover, Drawn — рисованная", false, false, (ValueValidator)null, (string)null);
			obj.SaveToFile(false);
			((MelonEventBase<LemonAction<HeadingStyle, HeadingStyle>>)(object)HeadingStyleEntry.OnEntryValueChanged).Subscribe((LemonAction<HeadingStyle, HeadingStyle>)delegate
			{
				FontPatcher.Reapply();
			}, 0, false);
			((MelonEventBase<LemonAction<float, float>>)(object)HeadingScaleEntry.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate
			{
				FontPatcher.Reapply();
			}, 0, false);
			((MelonEventBase<LemonAction<float, float>>)(object)TextScaleEntry.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate
			{
				FontPatcher.Reapply();
			}, 0, false);
			if (!EnabledEntry.Value)
			{
				Log.Msg("disabled");
				return;
			}
			int num = FontFiles.Extract();
			Log.Msg((num > 0) ? $"ready — {num} font file(s)" : "no font files inside the dll — nothing to do");
			if (MelonBase.FindMelon("Mod Menu", "Relsev") == null)
			{
				return;
			}
			try
			{
				MenuPage.Register();
			}
			catch (Exception ex)
			{
				Log.Warning("Mod Menu page: " + ex.Message);
			}
		}

		public override void OnSceneWasLoaded(int buildIndex, string sceneName)
		{
			_nextScan = 0f;
		}

		public override void OnUpdate()
		{
			if (!EnabledEntry.Value || Time.unscaledTime < _nextScan)
			{
				return;
			}
			_nextScan = Time.unscaledTime + 2f;
			try
			{
				FontPatcher.Scan();
			}
			catch (Exception ex)
			{
				Log.Error((object)ex);
				_nextScan = float.MaxValue;
			}
		}
	}
}