Some mods target the Mono version of the game, which is available by opting into the Steam beta branch "alternate"
Decompiled source of ScheduleI PTBR v1.5.7
Plugins/ScheduleI_PTBR.dll
Decompiled 15 hours agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using Il2CppInterop.Runtime; using Il2CppInterop.Runtime.InteropTypes; using MelonLoader; using MelonLoader.Preferences; using MelonLoader.Utils; using Microsoft.CodeAnalysis; using ScheduleI_PTBR; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: MelonInfo(typeof(Plugin), "ScheduleI_PTBR", "1.1.0", "LordValdomod", null)] [assembly: MelonGame("TVGS", "Schedule I")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("ScheduleI_PTBR")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.1.0.0")] [assembly: AssemblyInformationalVersion("1.1.0")] [assembly: AssemblyProduct("ScheduleI_PTBR")] [assembly: AssemblyTitle("ScheduleI_PTBR")] [assembly: AssemblyVersion("1.1.0.0")] [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 ScheduleI_PTBR { internal static class Dumper { private class Target { public Type Type; public object Il2CppType; public PropertyInfo Text; } private const int IntervalMs = 1000; private static readonly Stopwatch Clock = Stopwatch.StartNew(); private static readonly List<Target> Targets = new List<Target>(); private static HashSet<string> seen; private static HashSet<string> ignore; private static StreamWriter writer; private static MethodInfo findObjects; private static long nextScan; private static bool active; private static int errors; public static void Init(string path, HashSet<string> translatedValues) { ignore = translatedValues; seen = new HashSet<string>(StringComparer.Ordinal); if (File.Exists(path)) { foreach (string item in File.ReadLines(path)) { int num = TextFormat.FindSeparator(item); seen.Add((num >= 0) ? item.Substring(0, num) : item); } } writer = new StreamWriter(path, append: true, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)) { AutoFlush = true }; Type type = FindType("UnityEngine.Object"); string[] array = new string[2] { "Il2CppTMPro.TMP_Text", "UnityEngine.UI.Text" }; for (int i = 0; i < array.Length; i++) { Type type2 = FindType(array[i]); PropertyInfo propertyInfo = type2?.GetProperty("text", BindingFlags.Instance | BindingFlags.Public); if (type2 == null || propertyInfo == null) { continue; } object obj = ToIl2CppType(type2); if (obj != null) { if (findObjects == null && type != null) { findObjects = type.GetMethod("FindObjectsOfType", BindingFlags.Static | BindingFlags.Public, null, new Type[1] { obj.GetType() }, null); } Targets.Add(new Target { Type = type2, Il2CppType = obj, Text = propertyInfo }); } } active = findObjects != null && Targets.Count > 0; Plugin.Log.Msg(active ? $"Modo dump ativo ({Targets.Count} tipos de texto, varredura a cada {1000} ms). Arquivo: {path}" : "Modo dump não iniciado: tipos de texto do jogo não encontrados."); } private static object ToIl2CppType(Type t) { MethodInfo methodInfo = (from m in typeof(Il2CppObjectBase).Assembly.GetType("Il2CppInterop.Runtime.Il2CppType")?.GetMethods(BindingFlags.Static | BindingFlags.Public) where m.Name == "From" && m.GetParameters().FirstOrDefault()?.ParameterType == typeof(Type) orderby m.GetParameters().Length select m).FirstOrDefault(); if (methodInfo == null) { return null; } object[] parameters = methodInfo.GetParameters().Select((ParameterInfo p, int i) => (i != 0) ? ((!(p.ParameterType == typeof(bool))) ? p.DefaultValue : ((object)false)) : t).ToArray(); return methodInfo.Invoke(null, parameters); } public static void Tick() { if (!active || Clock.ElapsedMilliseconds < nextScan) { return; } nextScan = Clock.ElapsedMilliseconds + 1000; foreach (Target target in Targets) { try { if (!(findObjects.Invoke(null, new object[1] { target.Il2CppType }) is IEnumerable enumerable)) { continue; } foreach (object item in enumerable) { Il2CppObjectBase val = (Il2CppObjectBase)((item is Il2CppObjectBase) ? item : null); if (val != null && !(val.Pointer == IntPtr.Zero)) { object obj = Activator.CreateInstance(target.Type, val.Pointer); Record(target.Text.GetValue(obj) as string); } } } catch (Exception ex) { if (++errors <= 3) { Plugin.Log.Warning("Falha na varredura de " + target.Type.Name + ": " + (ex.InnerException?.Message ?? ex.Message)); } if (errors >= 20) { active = false; Plugin.Log.Error("Modo dump desativado após falhas repetidas."); } } } } public static void Close() { try { writer?.Dispose(); } catch { } } private static Type FindType(string fullName) { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); for (int i = 0; i < assemblies.Length; i++) { Type type = assemblies[i].GetType(fullName, throwOnError: false); if (type != null) { return type; } } string[] array = new string[3] { "UnityEngine.CoreModule", "Unity.TextMeshPro", "UnityEngine.UI" }; foreach (string assemblyString in array) { try { Type type2 = Assembly.Load(assemblyString).GetType(fullName, throwOnError: false); if (type2 != null) { return type2; } } catch { } } return null; } private static void Record(string text) { if (!string.IsNullOrWhiteSpace(text) && text.Length <= 4000 && text.Any(char.IsLetter)) { string text2 = TextFormat.Escape(text); if (!ignore.Contains(text2) && seen.Add(text2)) { writer.WriteLine(text2 + "="); } } } } internal static class FullScan { private const int MaxDepth = 4; private const int DelayMs = 15000; private static readonly Stopwatch Clock = Stopwatch.StartNew(); private static readonly Dictionary<string, Type> TypeIndex = new Dictionary<string, Type>(StringComparer.Ordinal); private static readonly Dictionary<Type, List<PropertyInfo>> PropCache = new Dictionary<Type, List<PropertyInfo>>(); private static readonly Dictionary<string, SortedSet<string>> Found = new Dictionary<string, SortedSet<string>>(StringComparer.Ordinal); private static HashSet<string> ignore; private static string outPath; private static long scanAt = -1L; private static int scans; private static MethodInfo findAll; private static Type unityObject; public static void Init(string path, HashSet<string> translatedValues) { outPath = path; ignore = translatedValues; unityObject = FindType("UnityEngine.Object"); findAll = FindType("UnityEngine.Resources")?.GetMethods(BindingFlags.Static | BindingFlags.Public).FirstOrDefault((MethodInfo m) => m.Name == "FindObjectsOfTypeAll" && !m.IsGenericMethod && m.GetParameters().Length == 1); if (findAll == null || unityObject == null) { Plugin.Log.Error("Varredura completa indisponível: API da Unity não encontrada."); return; } if (File.Exists(path)) { string section = ""; foreach (string item in File.ReadLines(path)) { if (item.StartsWith("//")) { section = item.Substring(2); continue; } int num = TextFormat.FindSeparator(item); if (num > 0) { Add(section, item.Substring(0, num), escaped: true); } } } ((MelonEventBase<LemonAction<int, string>>)(object)MelonEvents.OnSceneWasInitialized).Subscribe((LemonAction<int, string>)delegate(int i, string name) { Schedule(name); }, 0, false); Plugin.Log.Msg("Varredura completa ativa. Roda 15 s depois de cada cena carregar. Arquivo: " + path); } private static void Schedule(string scene) { scanAt = Clock.ElapsedMilliseconds + 15000; Plugin.Log.Msg("Cena '" + scene + "' carregada, varredura completa em 15 s..."); } public static void Tick() { if (scanAt >= 0 && Clock.ElapsedMilliseconds >= scanAt) { scanAt = -1L; Stopwatch stopwatch = Stopwatch.StartNew(); int num = Found.Values.Sum((SortedSet<string> s) => s.Count); try { ScanTexts(); } catch (Exception ex) { Plugin.Log.Warning("Falha nos textos de UI: " + ex.Message); } try { ScanObjects("UnityEngine.ScriptableObject"); } catch (Exception ex2) { Plugin.Log.Warning("Falha nos dados do jogo: " + ex2.Message); } try { ScanObjects("UnityEngine.MonoBehaviour"); } catch (Exception ex3) { Plugin.Log.Warning("Falha nos componentes: " + ex3.Message); } Save(); scans++; int num2 = Found.Values.Sum((SortedSet<string> s) => s.Count); Plugin.Log.Msg($"Varredura #{scans} concluída em {stopwatch.ElapsedMilliseconds} ms: {num2 - num} textos novos, {num2} no total."); } } private static IEnumerable<Il2CppObjectBase> FindAll(string typeName) { Type type = FindType(typeName); if (type == null) { yield break; } object obj = ToIl2CppType(type); if (obj == null || !(findAll.Invoke(null, new object[1] { obj }) is IEnumerable enumerable)) { yield break; } foreach (object item in enumerable) { Il2CppObjectBase val = (Il2CppObjectBase)((item is Il2CppObjectBase) ? item : null); if (val != null && val.Pointer != IntPtr.Zero) { yield return val; } } } private static void ScanTexts() { string[] array = new string[2] { "Il2CppTMPro.TMP_Text", "UnityEngine.UI.Text" }; foreach (string text in array) { Type type = FindType(text); PropertyInfo propertyInfo = type?.GetProperty("text", BindingFlags.Instance | BindingFlags.Public); if (propertyInfo == null) { continue; } foreach (Il2CppObjectBase item in FindAll(text)) { try { Add("UI", propertyInfo.GetValue(Activator.CreateInstance(type, item.Pointer)) as string); } catch { } } } } private static void ScanObjects(string baseType) { foreach (Il2CppObjectBase item in FindAll(baseType)) { try { Type type = ManagedTypeOf(item.Pointer); if (!(type == null) && type.FullName.StartsWith("Il2CppScheduleOne", StringComparison.Ordinal)) { Walk(Activator.CreateInstance(type, item.Pointer), type, type.Name, 0, new HashSet<IntPtr> { item.Pointer }); } } catch { } } } private static void Walk(object obj, Type t, string section, int depth, HashSet<IntPtr> seen) { foreach (PropertyInfo item in FieldProps(t)) { object value; try { value = item.GetValue(obj); } catch { continue; } if (value == null) { continue; } if (value is string text) { Add(section, text); } else { if (depth >= 4) { continue; } if (value is IEnumerable enumerable && !(value is Il2CppObjectBase)) { foreach (object item2 in enumerable) { Visit(item2, section, depth, seen); } } else { Visit(value, section, depth, seen); } } } } private static void Visit(object v, string section, int depth, HashSet<IntPtr> seen) { if (v is string text) { Add(section, text); return; } Il2CppObjectBase val = (Il2CppObjectBase)((v is Il2CppObjectBase) ? v : null); if (val == null || val.Pointer == IntPtr.Zero || !seen.Add(val.Pointer)) { return; } Type type = v.GetType(); if (unityObject.IsAssignableFrom(type)) { return; } if (type.Name.StartsWith("List`1") || type.Name.StartsWith("Il2CppStringArray") || type.Name.StartsWith("Il2CppReferenceArray")) { foreach (object item in Items(v)) { Visit(item, section, depth + 1, seen); } return; } if (type.FullName != null && type.FullName.StartsWith("Il2CppScheduleOne", StringComparison.Ordinal)) { Walk(v, type, section, depth + 1, seen); } } private static IEnumerable<object> Items(object list) { Type type = list.GetType(); PropertyInfo propertyInfo = type.GetProperty("Count") ?? type.GetProperty("Length"); MethodInfo getter = type.GetMethod("get_Item", new Type[1] { typeof(int) }); if (propertyInfo == null || getter == null) { yield break; } int n; try { n = Convert.ToInt32(propertyInfo.GetValue(list)); } catch { yield break; } for (int i = 0; i < Math.Min(n, 5000); i++) { object obj2 = null; try { obj2 = getter.Invoke(list, new object[1] { i }); } catch { } if (obj2 != null) { yield return obj2; } } } private static List<PropertyInfo> FieldProps(Type t) { if (PropCache.TryGetValue(t, out var value)) { return value; } value = new List<PropertyInfo>(); Type type = t; while (type != null && type != typeof(object) && type.FullName != null && type.FullName.StartsWith("Il2CppScheduleOne", StringComparison.Ordinal)) { PropertyInfo[] properties = type.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public); foreach (PropertyInfo propertyInfo in properties) { if (propertyInfo.GetIndexParameters().Length == 0 && propertyInfo.CanRead && !(type.GetField("NativeFieldInfoPtr_" + propertyInfo.Name, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) == null)) { value.Add(propertyInfo); } } type = type.BaseType; } PropCache[t] = value; return value; } private static Type ManagedTypeOf(IntPtr obj) { IntPtr intPtr = IL2CPP.il2cpp_object_get_class(obj); if (intPtr == IntPtr.Zero) { return null; } string text = Marshal.PtrToStringAnsi(IL2CPP.il2cpp_class_get_namespace(intPtr)) ?? ""; string text2 = Marshal.PtrToStringAnsi(IL2CPP.il2cpp_class_get_name(intPtr)) ?? ""; string text3 = ((text.Length > 0) ? (text + "." + text2) : text2); if (TypeIndex.TryGetValue(text3, out var value)) { return value; } value = FindType("Il2Cpp" + text3) ?? FindType(text3); TypeIndex[text3] = value; return value; } private static void Add(string section, string text, bool escaped = false) { if (text == null) { return; } string item = (escaped ? text : TextFormat.Escape(text)); if (escaped || (!string.IsNullOrWhiteSpace(text) && text.Length <= 4000 && text.Any(char.IsLetter) && !ignore.Contains(item))) { if (!Found.TryGetValue(section, out var value)) { value = (Found[section] = new SortedSet<string>(StringComparer.Ordinal)); } value.Add(item); } } private static void Save() { StringBuilder stringBuilder = new StringBuilder(); HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); foreach (KeyValuePair<string, SortedSet<string>> item in Found.OrderBy<KeyValuePair<string, SortedSet<string>>, string>((KeyValuePair<string, SortedSet<string>> k) => (!(k.Key == "UI")) ? k.Key : "", StringComparer.Ordinal)) { List<string> list = item.Value.Where(hashSet.Add).ToList(); if (list.Count == 0) { continue; } stringBuilder.Append("//").Append(item.Key).Append('\n'); foreach (string item2 in list) { stringBuilder.Append(item2).Append("=\n"); } } File.WriteAllText(outPath, stringBuilder.ToString(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); } private static object ToIl2CppType(Type t) { MethodInfo methodInfo = (from m in typeof(Il2CppObjectBase).Assembly.GetType("Il2CppInterop.Runtime.Il2CppType")?.GetMethods(BindingFlags.Static | BindingFlags.Public) where m.Name == "From" && m.GetParameters().FirstOrDefault()?.ParameterType == typeof(Type) orderby m.GetParameters().Length select m).FirstOrDefault(); if (methodInfo == null) { return null; } object[] parameters = methodInfo.GetParameters().Select((ParameterInfo p, int i) => (i != 0) ? ((!(p.ParameterType == typeof(bool))) ? p.DefaultValue : ((object)false)) : t).ToArray(); return methodInfo.Invoke(null, parameters); } private static Type FindType(string fullName) { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { Type type = null; try { type = assembly.GetType(fullName, throwOnError: false); } catch { } if (type != null) { return type; } } string[] array = new string[4] { "UnityEngine.CoreModule", "Unity.TextMeshPro", "UnityEngine.UI", "Assembly-CSharp" }; foreach (string assemblyString in array) { try { Type type2 = Assembly.Load(assemblyString).GetType(fullName, throwOnError: false); if (type2 != null) { return type2; } } catch { } } return null; } } public class Plugin : MelonPlugin { internal const string Id = "ScheduleI_PTBR"; private const string StampName = ".scheduleI_ptbr"; private const string LangDir = "Translation/pt/Text"; internal static Instance Log; private MelonPreferences_Entry<bool> dumpMode; private MelonPreferences_Entry<bool> fullScan; private string atRoot; private List<string> deployedFiles = new List<string>(); public override void OnPreInitialization() { Log = ((MelonBase)this).LoggerInstance; MelonPreferences_Category val = MelonPreferences.CreateCategory("ScheduleI_PTBR", "Tradução PT-BR"); dumpMode = val.CreateEntry<bool>("DumpMode", false, "Modo dump", "Registra os textos exibidos em UserData/ScheduleI_PTBR_dump.txt", false, false, (ValueValidator)null, (string)null); fullScan = val.CreateEntry<bool>("FullScan", false, "Varredura completa", "Lê todos os textos e dados carregados na memória e grava em UserData/ScheduleI_PTBR_fullscan.txt", false, false, (ValueValidator)null, (string)null); atRoot = Path.Combine(MelonEnvironment.GameRootDirectory, "AutoTranslator"); try { Deploy(); } catch (Exception ex) { Log.Error("Falha ao instalar as traduções: " + ex); } try { PatchConfig(); } catch (Exception ex2) { Log.Error("Falha ao ajustar o Config.ini: " + ex2); } } public override void OnApplicationStarted() { if (fullScan.Value) { try { FullScan.Init(Path.Combine(MelonEnvironment.UserDataDirectory, "ScheduleI_PTBR_fullscan.txt"), LoadTranslatedValues()); } catch (Exception ex) { Log.Error("Falha ao iniciar a varredura completa: " + ex); } } if (!dumpMode.Value) { return; } try { Dumper.Init(Path.Combine(MelonEnvironment.UserDataDirectory, "ScheduleI_PTBR_dump.txt"), LoadTranslatedValues()); } catch (Exception ex2) { Log.Error("Falha ao iniciar o modo dump: " + ex2); } } public override void OnUpdate() { if (dumpMode != null && dumpMode.Value) { Dumper.Tick(); } if (fullScan != null && fullScan.Value) { FullScan.Tick(); } } public override void OnApplicationQuit() { Dumper.Close(); } private string FindSource() { string text = Path.Combine(MelonEnvironment.UserDataDirectory, "ScheduleI_PTBR"); if (Directory.Exists(Path.Combine(text, "Translation/pt/Text"))) { return text; } foreach (string item in Directory.EnumerateDirectories(MelonEnvironment.MelonBaseDirectory, "ScheduleI_PTBR", SearchOption.AllDirectories)) { if (Directory.Exists(Path.Combine(item, "Translation/pt/Text"))) { return item; } } return null; } private void Deploy() { string text = FindSource(); if (text == null) { Log.Error("Pasta de traduções não encontrada. Reinstale o pacote."); return; } string path = Path.Combine(text, "Translation/pt/Text"); string dstText = Path.Combine(atRoot, "Translation/pt/Text"); Directory.CreateDirectory(dstText); List<string> list = Directory.GetFiles(path, "*.txt").OrderBy<string, string>((string f) => Path.GetFileName(f), StringComparer.Ordinal).ToList(); string text2 = HashFiles(list); string path2 = Path.Combine(atRoot, ".scheduleI_ptbr"); Dictionary<string, string> dictionary = ReadStamp(path2); List<string> names = list.Select(Path.GetFileName).ToList(); if (!dictionary.TryGetValue("hash", out var value) || !(value == text2) || !names.All((string n) => File.Exists(Path.Combine(dstText, n)))) { if (dictionary.TryGetValue("files", out var value2)) { foreach (string item in from n in value2.Split('|') where n.Length > 0 && !names.Contains(n) select n) { string path3 = Path.Combine(dstText, item); if (File.Exists(path3)) { File.Delete(path3); } } } foreach (string item2 in list) { File.Copy(item2, Path.Combine(dstText, Path.GetFileName(item2)), overwrite: true); } File.WriteAllText(path2, "hash=" + text2 + "\nfiles=" + string.Join("|", names) + "\n", new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); Log.Msg($"Traduções instaladas ({list.Count} arquivos)."); } deployedFiles = names.Select((string n) => Path.Combine(dstText, n)).ToList(); List<string> list2 = (from n in Directory.GetFiles(dstText, "*.txt").Select(Path.GetFileName) where !names.Contains(n) && !n.StartsWith("_") select n).ToList(); if (list2.Count > 0) { Log.Warning("Arquivos de outras traduções em AutoTranslator/Translation/pt/Text podem conflitar: " + string.Join(", ", list2)); } } private void PatchConfig() { Directory.CreateDirectory(atRoot); string text = Path.Combine(atRoot, "Config.ini"); List<string> list = (File.Exists(text) ? File.ReadAllLines(text).ToList() : new List<string>()); string text2 = string.Join("\n", list); string text3 = text + ".ptbr-bak"; if (File.Exists(text) && !File.Exists(text3)) { File.Copy(text, text3); } SetIni(list, "Service", "Endpoint", ""); SetIni(list, "Service", "FallbackEndpoint", ""); SetIni(list, "General", "Language", "pt"); SetIni(list, "General", "FromLanguage", "en"); if (string.Join("\n", list) != text2) { File.WriteAllLines(text, list, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); Log.Msg("Config.ini do AutoTranslator ajustado para PT-BR."); } } private static void SetIni(List<string> lines, string section, string key, string value) { string header = "[" + section + "]"; int num = lines.FindIndex((string l) => l.Trim().Equals(header, StringComparison.OrdinalIgnoreCase)); if (num < 0) { lines.Add(header); lines.Add(key + "=" + value); return; } int num2 = lines.FindIndex(num + 1, (string l) => l.TrimStart().StartsWith("[")); if (num2 < 0) { num2 = lines.Count; } for (int num3 = num + 1; num3 < num2; num3++) { if (lines[num3].TrimStart().StartsWith(key + "=", StringComparison.OrdinalIgnoreCase)) { lines[num3] = key + "=" + value; return; } } lines.Insert(num + 1, key + "=" + value); } private HashSet<string> LoadTranslatedValues() { HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); foreach (string item in deployedFiles.Where(File.Exists)) { foreach (string item2 in File.ReadLines(item)) { if (!item2.StartsWith("//") && !item2.StartsWith("r:") && !item2.StartsWith("sr:")) { int num = TextFormat.FindSeparator(item2); if (num > 0 && num < item2.Length - 1) { hashSet.Add(item2.Substring(num + 1)); } } } } return hashSet; } private static string HashFiles(List<string> files) { using SHA256 sHA = SHA256.Create(); foreach (string file in files) { byte[] bytes = Encoding.UTF8.GetBytes(Path.GetFileName(file)); sHA.TransformBlock(bytes, 0, bytes.Length, null, 0); byte[] array = File.ReadAllBytes(file); sHA.TransformBlock(array, 0, array.Length, null, 0); } sHA.TransformFinalBlock(Array.Empty<byte>(), 0, 0); return BitConverter.ToString(sHA.Hash).Replace("-", ""); } private static Dictionary<string, string> ReadStamp(string path) { Dictionary<string, string> dictionary = new Dictionary<string, string>(); if (!File.Exists(path)) { return dictionary; } string[] array = File.ReadAllLines(path); foreach (string text in array) { int num = text.IndexOf('='); if (num > 0) { dictionary[text.Substring(0, num)] = text.Substring(num + 1); } } return dictionary; } } internal static class TextFormat { public static string Escape(string s) { StringBuilder stringBuilder = new StringBuilder(s.Length + 8); string text = s.Replace("\r\n", "\n"); foreach (char c in text) { switch (c) { case '\\': stringBuilder.Append("\\\\"); break; case '\n': stringBuilder.Append("\\n"); break; case '\r': stringBuilder.Append("\\r"); break; case '=': stringBuilder.Append("\\="); break; default: stringBuilder.Append(c); break; } } return stringBuilder.ToString(); } public static int FindSeparator(string line) { for (int i = 0; i < line.Length; i++) { if (line[i] == '\\') { i++; } else if (line[i] == '=') { return i; } } return -1; } } }