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