using System; 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.Permissions; using System.Text.RegularExpressions; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using CSVFile; using HarmonyLib; using Microsoft.CodeAnalysis; using Pigeon.Movement; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: IgnoresAccessChecksTo("Assembly-CSharp")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyMetadata("AI_Assisted_Creation", "This assembly was partially or fully created with the assistance of Generative AI.")] [assembly: AssemblyMetadata("AI_Model_Vendor", "Anthropic")] [assembly: AssemblyCompany("NoBlasphemy")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+f8a12f66cd84cb9bb5142512e181af7111bc681e")] [assembly: AssemblyProduct("NoBlasphemy")] [assembly: AssemblyTitle("NoBlasphemy")] [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.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace NoBlasphemy { internal static class Filter { private static readonly string[] Reverent = new string[2] { "exch_faith_5", "vl_building_2" }; private static readonly HashSet blocked = new HashSet(); private static readonly HashSet mutedNames = new HashSet(StringComparer.OrdinalIgnoreCase); private static readonly Dictionary durations = new Dictionary(StringComparer.OrdinalIgnoreCase); private static Dictionary mutedIds; public static readonly List<(string group, int index, string variant, string id)> Testable = new List<(string, int, string, string)>(); private static readonly Dictionary alternatives = new Dictionary(); public static bool IsMuted(TextBlock block) { if (Plugin.MuteVoice.Value && block != null) { return blocked.Contains(block); } return false; } public static bool IsMuted(string eventName) { if (Plugin.MuteVoice.Value && eventName != null) { return mutedNames.Contains(eventName); } return false; } public static bool IsMuted(uint eventId, out string name) { name = null; if (!Plugin.MuteVoice.Value || mutedNames.Count == 0) { return false; } if (mutedIds == null) { mutedIds = new Dictionary(); foreach (string mutedName in mutedNames) { mutedIds[AkUnitySoundEngine.GetIDFromString(mutedName)] = mutedName; } } return mutedIds.TryGetValue(eventId, out name); } public static float Duration(string name) { if (name == null || !durations.TryGetValue(name, out var value)) { return 3f; } return value; } public static void Apply() { blocked.Clear(); mutedNames.Clear(); durations.Clear(); mutedIds = null; HashSet keep = Ids(Plugin.KeepLines.Value); HashSet extra = Ids(Plugin.ExtraLines.Value); keep.UnionWith(Reverent); Dictionary english = ReadEnglish(); int language = TextBlocks.LanguageIndex; bool cleanText = Plugin.CleanSubtitles.Value; Dictionary everyLineBlocked = new Dictionary(StringComparer.OrdinalIgnoreCase); int cleaned = 0; List<(string group, int index, string variant, string id)> found = new List<(string, int, string, string)>(); alternatives.Clear(); foreach (KeyValuePair @string in TextBlocks.strings) { TextBlockGroup value = @string.Value; HashSet hashSet = new HashSet(); if (value.variations != null) { foreach (KeyValuePair variation in value.variations) { hashSet.Add(variation.Value); for (int i = 0; i < variation.Value.Length; i++) { if (variation.Value[i] != null) { Consider(variation.Value[i], variation.Key, @string.Key, i); } } Alternatives(variation.Value); } } if (value.blocks == null || hashSet.Contains(value.blocks)) { continue; } for (int j = 0; j < value.blocks.Length; j++) { if (value.blocks[j] != null) { Consider(value.blocks[j], null, @string.Key, j); } } Alternatives(value.blocks); } foreach (KeyValuePair item in everyLineBlocked) { if (item.Value) { mutedNames.Add(item.Key); } } Testable.Clear(); Testable.AddRange(from f in found where mutedNames.Contains(f.id) orderby f.id select f); Plugin.Log.LogInfo((object)(string.Format("{0} {1}: {2} lines found, {3} subtitles cleaned, ", "Quad's No Blasphemy", "1.0.0", blocked.Count, cleaned) + $"{(Plugin.SwapLines.Value ? alternatives.Count : 0)} swapped for another line when picked at random, " + (Plugin.MuteVoice.Value ? "otherwise silenced" : "voice left on (MuteVoice = false)"))); static void Alternatives(TextBlock[] lines) { List list = lines.Where((TextBlock b) => b != null && !blocked.Contains(b)).ToList(); if (list.Count != 0) { for (int num = 0; num < lines.Length; num++) { if (lines[num] != null && blocked.Contains(lines[num])) { alternatives[lines[num]] = list[num % list.Count]; } } } } void Consider(TextBlock block, string variant, string groupKey, int index) { string uniqueID = block.uniqueID; if (!english.TryGetValue(Key(uniqueID, variant), out var value2) && !english.TryGetValue(Key(uniqueID, null), out value2)) { value2 = ((language == 0) ? block.text : null); } bool flag = keep.Contains(uniqueID); bool flag2 = extra.Contains(uniqueID) || (!flag && Words.IsBlasphemous(value2)); bool num = flag2 || (!flag && language != 0 && Words.IsBlasphemous(block.text, language)); everyLineBlocked[uniqueID] = (!everyLineBlocked.TryGetValue(uniqueID, out var value3) || value3) && flag2; if (flag2) { blocked.Add(block); found.Add((groupKey, index, variant, uniqueID)); durations[uniqueID] = Mathf.Clamp(1f + (float)(value2 ?? block.text).Length * 0.055f, 2f, 12f); } if (num) { if (cleanText) { string text = block.text; block.text = Words.Clean(block.text, language); block.variations = null; if (block.text != text) { cleaned++; } } if (Plugin.LogLines.Value) { Plugin.Log.LogInfo((object)(" " + uniqueID + ((variant != null) ? (" [" + variant + "]") : "") + (flag2 ? "" : " (text only)") + ": " + block.text)); } } } } public static TextBlock Swap(TextBlock block) { if (block == null || !Plugin.SwapLines.Value || !alternatives.TryGetValue(block, out var value)) { return block; } if (Plugin.LogLines.Value) { Plugin.Log.LogInfo((object)("No Blasphemy: said another line instead of " + block.uniqueID)); } return value; } private static Dictionary ReadEnglish() { Dictionary dictionary = new Dictionary(); TextAsset val = Resources.Load("TextBlocks"); if ((Object)(object)val == (Object)null) { Plugin.Log.LogWarning((object)"No Blasphemy: the game's text table wasn't found; only the shown language's text is checked"); return dictionary; } try { CSVReader val2 = CSVReader.FromString(val.text, (CSVSettings)null); try { foreach (string[] item in val2.Lines()) { if (item.Length >= 3 && !string.IsNullOrEmpty(item[0])) { string text = item[0].Trim(); string variant = null; int num = text.LastIndexOf('['); if (text.EndsWith("]") && num > 0) { variant = text.Substring(num + 1, text.Length - num - 2); text = text.Substring(0, num).TrimEnd(); } dictionary[Key(text, variant)] = item[2]; } } return dictionary; } finally { ((IDisposable)val2)?.Dispose(); } } finally { Resources.UnloadAsset((Object)(object)val); } } private static string Key(string id, string variant) { return id + "\n" + variant; } private static HashSet Ids(string list) { return new HashSet(from s in (list ?? "").Split(',') select s.Trim() into s where s.Length > 0 select s, StringComparer.OrdinalIgnoreCase); } } [HarmonyPatch(typeof(TextBlocks), "Initialize")] internal static class TextLoaded { private static void Postfix() { Filter.Apply(); } } [HarmonyPatch(typeof(TextBlockGroup), "Get", new Type[] { })] internal static class PickAny { private static void Postfix(ref TextBlock __result) { __result = Filter.Swap(__result); } } [HarmonyPatch(/*Could not decode attribute arguments.*/)] internal static class PickSeeded { private static void Postfix(ref TextBlock __result) { __result = Filter.Swap(__result); } } [HarmonyPatch(/*Could not decode attribute arguments.*/)] internal static class PickCharacterLine { private static void Postfix(string parameter, ref TextBlock block, ref string __result) { TextBlock val = Filter.Swap(block); if (val != block) { block = val; __result = val.GetText(parameter, true); } } } [HarmonyPatch(typeof(Highlighter), "PlayVoicelineSound", new Type[] { typeof(TextBlock), typeof(GameObject), typeof(string), typeof(EventCallback), typeof(object) })] internal static class PlayerLine { private static bool Prefix(TextBlock block, ref bool __result) { if (!Filter.IsMuted(block)) { return true; } if (Plugin.LogLines.Value) { Plugin.Log.LogInfo((object)("No Blasphemy: silenced " + block.uniqueID)); } __result = false; return false; } } [HarmonyPatch(typeof(AkUnitySoundEngine), "PostEvent", new Type[] { typeof(uint), typeof(GameObject) })] internal static class EventById { private static bool Prefix(uint in_eventID, ref uint __result) { return Sounds.Allow(in_eventID, 0u, null, null, ref __result); } } [HarmonyPatch(typeof(AkUnitySoundEngine), "PostEvent", new Type[] { typeof(uint), typeof(GameObject), typeof(uint), typeof(EventCallback), typeof(object) })] internal static class EventByIdWithCallback { private static bool Prefix(uint in_eventID, uint in_uFlags, EventCallback in_pfnCallback, object in_pCookie, ref uint __result) { return Sounds.Allow(in_eventID, in_uFlags, in_pfnCallback, in_pCookie, ref __result); } } [HarmonyPatch(typeof(AkUnitySoundEngine), "PostEvent", new Type[] { typeof(string), typeof(GameObject) })] internal static class EventByName { private static bool Prefix(string in_pszEventName, ref uint __result) { return Sounds.Allow(in_pszEventName, 0u, null, null, ref __result); } } [HarmonyPatch(typeof(AkUnitySoundEngine), "PostEvent", new Type[] { typeof(string), typeof(GameObject), typeof(uint), typeof(EventCallback), typeof(object) })] internal static class EventByNameWithCallback { private static bool Prefix(string in_pszEventName, uint in_uFlags, EventCallback in_pfnCallback, object in_pCookie, ref uint __result) { return Sounds.Allow(in_pszEventName, in_uFlags, in_pfnCallback, in_pCookie, ref __result); } } internal static class Sounds { public static bool Allow(uint id, uint flags, EventCallback callback, object cookie, ref uint result) { if (Filter.IsMuted(id, out var name)) { return Silence(name, flags, callback, cookie, ref result); } return true; } public static bool Allow(string name, uint flags, EventCallback callback, object cookie, ref uint result) { if (Filter.IsMuted(name)) { return Silence(name, flags, callback, cookie, ref result); } return true; } private static bool Silence(string name, uint flags, EventCallback callback, object cookie, ref uint result) { if (Plugin.LogLines.Value) { Plugin.Log.LogInfo((object)("No Blasphemy: silenced " + name)); } if ((flags & 1) != 0) { Later.Call(Filter.Duration(name), callback, cookie); } result = 0u; return false; } } [BepInPlugin("quadslab.mycopunk.noblasphemy", "Quad's No Blasphemy", "1.0.0")] [BepInProcess("Mycopunk.exe")] public class Plugin : BaseUnityPlugin { public const string Guid = "quadslab.mycopunk.noblasphemy"; public const string Title = "Quad's No Blasphemy"; public const string Version = "1.0.0"; internal static ManualLogSource Log; internal static ConfigEntry MuteVoice; internal static ConfigEntry CleanSubtitles; internal static ConfigEntry SwapLines; internal static ConfigEntry KeepLines; internal static ConfigEntry ExtraLines; internal static ConfigEntry LogLines; internal static ConfigEntry TestKey; private GameObject runner; private Harmony harmony; private void Awake() { //IL_00f9: Unknown result type (might be due to invalid IL or missing references) //IL_0103: Expected O, but got Unknown //IL_012d: Unknown result type (might be due to invalid IL or missing references) //IL_0137: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; MuteVoice = ((BaseUnityPlugin)this).Config.Bind("General", "MuteVoice", true, "Silence the spoken lines that use God's name in vain. A recording can't lose just one word, so the whole line is quiet; its subtitle still shows, cleaned."); CleanSubtitles = ((BaseUnityPlugin)this).Config.Bind("General", "CleanSubtitles", true, "Take God's name out of the subtitles, in every language the game has. Takes effect at the next start."); SwapLines = ((BaseUnityPlugin)this).Config.Bind("General", "SwapLines", true, "When a character has other lines for the same moment (a call for help), say one of those instead of going quiet."); KeepLines = ((BaseUnityPlugin)this).Config.Bind("Lines", "Keep", "", "More line IDs to leave alone, comma-separated (the lines that speak of God sincerely are always kept). Takes effect at the next start."); ExtraLines = ((BaseUnityPlugin)this).Config.Bind("Lines", "Extra", "", "More line IDs to clean and silence, comma-separated, for any line the word list misses. Takes effect at the next start."); LogLines = ((BaseUnityPlugin)this).Config.Bind("Debug", "LogLines", false, "Write every line changed (at start) and every line silenced (as it plays) to the BepInEx log, with its ID."); TestKey = ((BaseUnityPlugin)this).Config.Bind("Debug", "TestKey", false, "F8 in a game plays the next line this mod changes, as mission control would say it: you see its cleaned subtitle and hear nothing. (A file BepInEx\\noblasphemy-test.txt does the same, for automated tests.)"); runner = new GameObject("NoBlasphemy"); Object.DontDestroyOnLoad((Object)(object)runner); ((Object)runner).hideFlags = (HideFlags)61; runner.AddComponent(); harmony = new Harmony("quadslab.mycopunk.noblasphemy"); harmony.PatchAll(typeof(Plugin).Assembly); if (TextBlocks.strings.Count > 0) { Filter.Apply(); } } private void OnDestroy() { Harmony obj = harmony; if (obj != null) { obj.UnpatchSelf(); } if ((Object)(object)runner != (Object)null) { Object.Destroy((Object)(object)runner); } } } internal sealed class Later : MonoBehaviour { private static readonly List<(float at, EventCallback callback, object cookie)> pending = new List<(float, EventCallback, object)>(); private int nextTest; private float nextFileCheck; public static void Call(float delay, EventCallback callback, object cookie) { if (callback != null) { pending.Add((Time.unscaledTime + delay, callback, cookie)); } } private void Update() { if (Plugin.TestKey.Value && Keyboard.current != null && ((ButtonControl)Keyboard.current.f8Key).wasPressedThisFrame) { PlayTest(); } if (Plugin.TestKey.Value && Time.unscaledTime >= nextFileCheck) { nextFileCheck = Time.unscaledTime + 0.5f; string path = Path.Combine(Paths.BepInExRootPath, "noblasphemy-test.txt"); if (File.Exists(path)) { try { File.Delete(path); } catch (Exception) { } PlayTest(); } } for (int num = pending.Count - 1; num >= 0; num--) { if (!(Time.unscaledTime < pending[num].at)) { (float at, EventCallback callback, object cookie) tuple = pending[num]; EventCallback item = tuple.callback; object item2 = tuple.cookie; pending.RemoveAt(num); try { item.Invoke(item2, (AkCallbackType)1, (AkCallbackInfo)null); } catch (Exception ex2) { Plugin.Log.LogWarning((object)("No Blasphemy: a line's finish callback failed: " + ex2.Message)); } } } } private void PlayTest() { //IL_00b4: Unknown result type (might be due to invalid IL or missing references) if (Filter.Testable.Count == 0 || (Object)(object)PlayerLook.Instance == (Object)null || (Object)(object)Highlighter.Instance == (Object)null) { Plugin.Log.LogInfo((object)"No Blasphemy: the test key works once you're in the game"); return; } var (text, num, text2, arg) = Filter.Testable[nextTest++ % Filter.Testable.Count]; Plugin.Log.LogInfo((object)$"No Blasphemy: test {nextTest}/{Filter.Testable.Count}: {arg}"); try { Highlighter.Instance.PlayMissionControlVoiceline(new Voiceline(text, 1000), (string)null, 0, text2, (Predicate)null, (Action)null, 0f, num, (Action)null, false, (EventCallback)null, (object)null, (Sprite)null, (GameObject)null); } catch (Exception ex) { Plugin.Log.LogWarning((object)("No Blasphemy: the test line didn't play: " + ex.Message)); } } } public static class Words { private const RegexOptions Opts = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant; private static readonly (Regex pattern, MatchEvaluator replace)[][] Rules = new(Regex, MatchEvaluator)[10][] { new(Regex, MatchEvaluator)[10] { (new Regex("\\bgod[\\s-]*dam(?:n|m)?(it|ed)?\\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant), (Match m) => Cased(m.Value, (m.Groups[1].Value.ToLowerInvariant() == "it") ? "dang it" : "dang")), R("(?<=\\bmy\\s+)(god|lord|gawd)\\b", "gosh"), R("(?<=\\bthank\\s+)(god|gawd|the\\s+lord)\\b", "goodness"), R("\\bfor\\s+god'?s\\s+sake\\b", "for goodness' sake"), R("(?<=\\bgood\\s+)lord\\b", "grief"), R("(?<=\\b(?:oh|dear|sweet)\\s+)lord\\b", "gosh"), R("\\blord\\s+almighty\\b", "goodness gracious"), R("\\bomg\\b", "oh my gosh"), R("\\bgod\\s+help\\s+us\\b", "heaven help us"), R("\\b(god|gawd|jesus|christ|lordy)\\b", "gosh") }, new(Regex, MatchEvaluator)[6] { R("\\bnom\\s+de\\s+dieu\\b", "nom d'une pipe"), R("\\bbonté\\s+divine\\b", "bon sang"), R("\\b(?:oh\\s+)?mon\\s+dieu\\b", "oh là là"), R("\\bseigneur(?=\\s*[!,.…])", "bon sang"), R("\\bdieu(?=\\s*[!…])", "bon sang"), R("\\bj[ée]sus\\b", "purée") }, new(Regex, MatchEvaluator)[8] { R("\\bgottverdammt", "verflixt"), R("\\bum\\s+gottes\\s+willen\\b", "um Himmels willen"), R("\\bgott\\s+sei\\s+dank\\b", "zum Glück"), R("\\bmein\\s+gott\\b", "meine Güte"), R("\\boh\\s+gott\\b", "oh Mann"), R("\\bgrundgütiger\\b", "meine Güte"), R("\\bherrgott\\w*", "Mensch"), R("\\b(jessas|jesus)\\b", "Mannomann") }, new(Regex, MatchEvaluator)[7] { R("\\bdios\\s+m[ií]o\\b", "madre mía"), R("\\bgracias\\s+a\\s+dios\\b", "menos mal"), R("\\bpor\\s+dios\\b", "por favor"), R("\\bque\\s+dios\\s+nos\\s+\\w+", "que haya suerte"), R("\\bdios(?=\\s*[,!…])", "madre mía"), R("\\bhostias?\\b", "ostras"), R("\\bjes[uú]s\\b", "caramba") }, new(Regex, MatchEvaluator)[4] { R("\\bне\\s+слава\\s+богу\\b", "не так"), R("\\bслава\\s+богу\\b", "к счастью"), R("\\b(?:о\\s+)?боже(?:\\s+мой)?\\b", "ох"), R("\\b(?:о\\s+)?господи\\b", "ох") }, new(Regex, MatchEvaluator)[7] { R("\\bmeu\\s+deus(?:\\s+do\\s+c[ée]u)?\\b", "minha nossa"), R("\\bgraças\\s+a\\s+deus(?=\\s+\\p{L})", "ainda bem que"), R("\\bgraças\\s+a\\s+deus\\b", "ainda bem"), R("\\bpelo\\s+amor\\s+de\\s+deus\\b", "por favor"), R("\\bdeus\\s+do\\s+c[ée]u\\b", "caramba"), R("^senhor(?=\\s*[,!])", "caramba"), R("\\bjesus\\b", "caramba") }, new(Regex, MatchEvaluator)[4] { R("愿上帝保佑我们", "但愿我们走运"), R("我的上帝", "我的天"), R("上帝", "老天"), R("耶稣", "天哪") }, new(Regex, MatchEvaluator)[5] { R("願主保佑我們", "但願我們走運"), R("我的上帝", "我的天"), R("上帝", "老天"), R("主啊", "天啊"), R("耶穌", "天哪") }, new(Regex, MatchEvaluator)[3] { R("オーマイゴッド", "なんてこった"), R("ジーザス", "なんてこった"), R("神よ", "なんてこった") }, new(Regex, MatchEvaluator)[4] { R("하느님|하나님", "세상에"), R("오\\s*마이\\s*갓", "세상에"), R("지저스", "세상에"), R("주여", "세상에") } }; private static (Regex, MatchEvaluator) R(string pattern, string replacement) { return (new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant), (Match m) => Cased(m.Value, replacement)); } private static (Regex pattern, MatchEvaluator replace)[] For(int language) { if (language < 0 || language >= Rules.Length) { return Rules[0]; } return Rules[language]; } public static bool IsBlasphemous(string text, int language = 0) { if (!string.IsNullOrEmpty(text)) { return For(language).Any(((Regex pattern, MatchEvaluator replace) r) => r.pattern.IsMatch(text)); } return false; } public static string Clean(string text, int language = 0) { if (string.IsNullOrEmpty(text)) { return text; } (Regex, MatchEvaluator)[] array = For(language); for (int i = 0; i < array.Length; i++) { (Regex, MatchEvaluator) tuple = array[i]; Regex item = tuple.Item1; MatchEvaluator item2 = tuple.Item2; text = item.Replace(text, item2); } return text; } private static string Cased(string original, string replacement) { string text = original.Split(' ', '-', '\t', '\n')[^1]; if (IsShouted(original) || (text.Length > 1 && IsShouted(text))) { return replacement.ToUpperInvariant(); } if (char.IsUpper(original[0])) { return char.ToUpperInvariant(replacement[0]) + replacement.Substring(1); } return replacement; } private static bool IsShouted(string s) { bool flag = false; foreach (char c in s) { if (char.IsLower(c)) { return false; } if (char.IsLetter(c)) { flag = true; } } if (flag) { return s.Length > 1; } return false; } } } namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] internal sealed class IgnoresAccessChecksToAttribute : Attribute { public IgnoresAccessChecksToAttribute(string assemblyName) { } } }