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;
}
}
}
}