Decompiled source of AsciiGuard v1.5.0

plugins/AsciiGuard.dll

Decompiled a year ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
using BepInEx;
using BepInEx.Logging;
using ComputerysModdingUtilities;
using HarmonyLib;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: AssemblyTitle("AsciiGuard")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("AsciiGuard")]
[assembly: AssemblyCopyright("Copyright ©  2025")]
[assembly: AssemblyTrademark("")]
[assembly: ComVisible(false)]
[assembly: Guid("8479685c-e0d3-496d-876d-ae107fa434a6")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: StraftatMod(true)]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace AsciiGuard;

[BepInPlugin("com.glizzmn.straftat.asciiguard", "ASCII Guard", "1.5.0")]
public class PluginMain : BaseUnityPlugin
{
	private class ReloadRunner : MonoBehaviour
	{
		private Action _tick;

		public void Init(Action tick)
		{
			_tick = tick;
		}

		private void Update()
		{
			_tick?.Invoke();
		}
	}

	public const string PluginGuid = "com.glizzmn.straftat.asciiguard";

	public const string PluginName = "ASCII Guard";

	public const string PluginVersion = "1.5.0";

	internal static ManualLogSource Log;

	internal static List<Regex> SlurRegexes;

	private static string _dir;

	private static string _confusablesPath;

	private FileSystemWatcher _watcher;

	private float _reloadAt;

	private bool _reloading;

	private void Awake()
	{
		//IL_005c: Unknown result type (might be due to invalid IL or missing references)
		Log = ((BaseUnityPlugin)this).Logger;
		SlurRegexes = SlurPatterns.BuildCompiled();
		_dir = Path.GetDirectoryName(((BaseUnityPlugin)this).Info.Location);
		_confusablesPath = Path.Combine(_dir, "confusables.json");
		Confusables.LoadFromJson(_confusablesPath, Log);
		SetupWatcher();
		new Harmony("com.glizzmn.straftat.asciiguard").PatchAll();
		Log.LogInfo((object)string.Format("{0} v{1} ready. Confusables mapped: {2}", "ASCII Guard", "1.5.0", Confusables.Count));
	}

	private void OnDestroy()
	{
		try
		{
			_watcher?.Dispose();
		}
		catch
		{
		}
	}

	private void SetupWatcher()
	{
		if (Directory.Exists(_dir))
		{
			_watcher = new FileSystemWatcher(_dir)
			{
				Filter = Path.GetFileName(_confusablesPath),
				NotifyFilter = (NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.LastWrite | NotifyFilters.CreationTime),
				IncludeSubdirectories = false,
				EnableRaisingEvents = true
			};
			_watcher.Changed += delegate
			{
				DebouncedReload();
			};
			_watcher.Created += delegate
			{
				DebouncedReload();
			};
			_watcher.Renamed += delegate
			{
				DebouncedReload();
			};
		}
	}

	private void DebouncedReload()
	{
		//IL_0038: Unknown result type (might be due to invalid IL or missing references)
		//IL_0042: Expected O, but got Unknown
		_reloadAt = Time.realtimeSinceStartup + 0.25f;
		if (_reloading)
		{
			return;
		}
		_reloading = true;
		GameObject host = new GameObject("AsciiGuardReloadHost");
		Object.DontDestroyOnLoad((Object)(object)host);
		host.AddComponent<ReloadRunner>().Init(delegate
		{
			if (Time.realtimeSinceStartup >= _reloadAt)
			{
				Confusables.LoadFromJson(_confusablesPath, Log);
				_reloading = false;
				Object.Destroy((Object)(object)host);
			}
		});
	}

	internal static string BuildSkeleton(string input, out List<(int start, int len)> map)
	{
		StringBuilder sb = new StringBuilder(input.Length);
		List<(int start, int len)> localMap = new List<(int, int)>(input.Length);
		int num = 0;
		while (num < input.Length)
		{
			if (input[num] == '<')
			{
				int num2 = input.IndexOf('>', num);
				if (num2 < 0)
				{
					Emit(input[num], num, 1);
					num++;
				}
				else
				{
					num = num2 + 1;
				}
				continue;
			}
			int s2 = num;
			int num3 = 1;
			int num4;
			if (char.IsHighSurrogate(input[num]) && num + 1 < input.Length && char.IsLowSurrogate(input[num + 1]))
			{
				num4 = char.ConvertToUtf32(input[num], input[num + 1]);
				num3 = 2;
			}
			else
			{
				num4 = input[num];
			}
			if (num4 == 32 || num4 == 45 || num4 == 95 || num4 == 46)
			{
				Emit((char)num4, s2, num3);
				num += num3;
				continue;
			}
			if ((num4 >= 48 && num4 <= 57) || (num4 >= 65 && num4 <= 90) || (num4 >= 97 && num4 <= 122))
			{
				Emit(char.ToLowerInvariant((char)num4), s2, num3);
				num += num3;
				continue;
			}
			if (num4 >= 127462 && num4 <= 127487)
			{
				Emit((char)(97 + (num4 - 127462)), s2, num3);
				num += num3;
				continue;
			}
			if (Confusables.TryMap(num4, out var ascii))
			{
				Emit(char.ToLowerInvariant(ascii), s2, num3);
				num += num3;
				continue;
			}
			string text = char.ConvertFromUtf32(num4).Normalize(NormalizationForm.FormKD);
			bool flag = false;
			foreach (char c2 in text)
			{
				if ((c2 >= '0' && c2 <= '9') || (c2 >= 'A' && c2 <= 'Z') || (c2 >= 'a' && c2 <= 'z'))
				{
					Emit(char.ToLowerInvariant(c2), s2, num3);
					flag = true;
				}
			}
			num += num3;
		}
		map = localMap;
		return sb.ToString();
		void Emit(char c, int s, int l)
		{
			sb.Append(c);
			localMap.Add((s, l));
		}
	}

	internal static void CensorMatchesInPlace(char[] orig, string skeleton, List<(int start, int len)> map)
	{
		if (skeleton.Length == 0 || map.Count == 0)
		{
			return;
		}
		bool[] array = new bool[orig.Length];
		foreach (Regex slurRegex in SlurRegexes)
		{
			foreach (Match item3 in slurRegex.Matches(skeleton))
			{
				int index = item3.Index;
				int num = item3.Index + item3.Length - 1;
				for (int i = index; i <= num && (uint)i < (uint)map.Count; i++)
				{
					(int start, int len) tuple = map[i];
					int item = tuple.start;
					int item2 = tuple.len;
					for (int j = 0; j < item2; j++)
					{
						int num2 = item + j;
						if ((uint)num2 < (uint)orig.Length && !InsideTag(orig, num2))
						{
							array[num2] = true;
						}
					}
				}
			}
		}
		for (int k = 0; k < orig.Length; k++)
		{
			if (!array[k])
			{
				continue;
			}
			int num3;
			for (int l = k + 1; l < orig.Length && orig[l] != '<'; l += num3)
			{
				UnicodeCategory unicodeCategory;
				if (char.IsHighSurrogate(orig[l]) && l + 1 < orig.Length && char.IsLowSurrogate(orig[l + 1]))
				{
					unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(new string(orig, l, 2), 0);
					num3 = 2;
				}
				else
				{
					unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(orig[l]);
					num3 = 1;
				}
				if (unicodeCategory != UnicodeCategory.NonSpacingMark && unicodeCategory != UnicodeCategory.EnclosingMark && unicodeCategory != UnicodeCategory.SpacingCombiningMark && unicodeCategory != UnicodeCategory.Format)
				{
					break;
				}
				for (int m = 0; m < num3; m++)
				{
					if (l + m < orig.Length)
					{
						array[l + m] = true;
					}
				}
			}
		}
		for (int n = 0; n < orig.Length; n++)
		{
			if (array[n])
			{
				orig[n] = '*';
			}
		}
	}

	private static bool InsideTag(char[] buf, int idx)
	{
		int num = idx;
		while (num >= 0 && buf[num] != '<' && buf[num] != '>')
		{
			num--;
		}
		if (num >= 0 && buf[num] == '<')
		{
			for (int i = idx; i < buf.Length && i - num <= 256; i++)
			{
				if (buf[i] == '>')
				{
					return true;
				}
			}
		}
		return false;
	}
}
internal static class Confusables
{
	private static Dictionary<int, char> _map = new Dictionary<int, char>();

	public static int Count => _map.Count;

	public static bool TryMap(int cp, out char ascii)
	{
		return _map.TryGetValue(cp, out ascii);
	}

	public static void LoadFromJson(string path, ManualLogSource log = null)
	{
		Dictionary<int, char> dictionary = new Dictionary<int, char>();
		try
		{
			if (!File.Exists(path))
			{
				if (log != null)
				{
					log.LogWarning((object)("confusables.json not found at " + path + " (optional)."));
				}
				_map = dictionary;
				return;
			}
			string input = File.ReadAllText(path, Encoding.UTF8);
			MatchCollection matchCollection = Regex.Matches(input, "\"([A-Za-z0-9])\"\\s*:\\s*\\[(.*?)\\]", RegexOptions.Singleline);
			foreach (Match item in matchCollection)
			{
				char c = item.Groups[1].Value[0];
				char value = char.ToLowerInvariant(c);
				string value2 = item.Groups[2].Value;
				foreach (Match item2 in Regex.Matches(value2, "\"([0-9A-Fa-f]{2,6})\""))
				{
					string value3 = item2.Groups[1].Value;
					if (int.TryParse(value3, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var result) && result >= 0 && result <= 1114111 && (result < 55296 || result > 57343))
					{
						dictionary[result] = value;
					}
				}
			}
			_map = dictionary;
			if (log != null)
			{
				log.LogInfo((object)$"Loaded {dictionary.Count} confusable codepoints from confusables.json");
			}
		}
		catch (Exception ex)
		{
			if (log != null)
			{
				log.LogError((object)("Failed to load confusables.json: " + ex.Message));
			}
		}
	}
}
internal static class SlurPatterns
{
	private static readonly string[] BaseWords = new string[13]
	{
		"nigger", "nigga", "kike", "spic", "faggot", "fag", "retard", "tranny", "shemale", "rape",
		"kys", "nazi", "hitler"
	};

	private static readonly Dictionary<char, string> Map = new Dictionary<char, string>
	{
		['a'] = "a@4",
		['b'] = "b8",
		['c'] = "c",
		['d'] = "d",
		['e'] = "e3",
		['f'] = "f",
		['g'] = "g9",
		['h'] = "h",
		['i'] = "i1!|",
		['j'] = "j",
		['k'] = "k",
		['l'] = "l1|",
		['m'] = "m",
		['n'] = "n",
		['o'] = "o0",
		['p'] = "p",
		['q'] = "q",
		['r'] = "r",
		['s'] = "s$5",
		['t'] = "t7+",
		['u'] = "u",
		['v'] = "v",
		['w'] = "w",
		['x'] = "x",
		['y'] = "y",
		['z'] = "z2",
		['0'] = "0",
		['1'] = "1",
		['2'] = "2",
		['3'] = "3",
		['4'] = "4",
		['5'] = "5",
		['6'] = "6",
		['7'] = "7",
		['8'] = "8",
		['9'] = "9"
	};

	private const string Sep = "[ \\-_.]*";

	private static string ClassFor(char c)
	{
		c = char.ToLowerInvariant(c);
		string value;
		string str = (Map.TryGetValue(c, out value) ? value : c.ToString());
		return "[" + Regex.Escape(str) + "]";
	}

	private static string BuildPattern(string word)
	{
		StringBuilder stringBuilder = new StringBuilder();
		for (int i = 0; i < word.Length; i++)
		{
			stringBuilder.Append(ClassFor(word[i]));
			if (i < word.Length - 1)
			{
				stringBuilder.Append("[ \\-_.]*");
			}
		}
		return stringBuilder.ToString();
	}

	public static List<Regex> BuildCompiled(IEnumerable<string> extra = null)
	{
		IEnumerable<string> source;
		if (extra != null)
		{
			source = BaseWords.Concat(extra);
		}
		else
		{
			IEnumerable<string> baseWords = BaseWords;
			source = baseWords;
		}
		IEnumerable<string> source2 = (from w in source
			select w.Trim().ToLowerInvariant() into w
			where w.Length > 0
			select w).Distinct();
		return source2.Select((string w) => new Regex(BuildPattern(w), RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant)).ToList();
	}
}
[HarmonyPatch]
public static class Patch_FilterSystem_FilterString
{
	private static MethodBase TargetMethod()
	{
		Type type = AccessTools.TypeByName("FilterSystem");
		return (type == null) ? null : AccessTools.Method(type, "FilterString", new Type[1] { typeof(string) }, (Type[])null);
	}

	private static bool Prefix(ref string input)
	{
		if (string.IsNullOrEmpty(input))
		{
			return true;
		}
		List<(int, int)> map;
		string text = PluginMain.BuildSkeleton(input, out map);
		if (text.Length == 0 || map.Count == 0)
		{
			return true;
		}
		char[] array = input.ToCharArray();
		PluginMain.CensorMatchesInPlace(array, text, map);
		input = new string(array);
		return true;
	}
}