Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of RecipeSync v1.0.0
RecipeSync.dll
Decompiled 2 weeks agousing System; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using UnityEngine; [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: AssemblyDescription("Keeps every player's Valheim recipe log in sync, including players who are offline.")] [assembly: AssemblyProduct("RecipeSync")] [assembly: AssemblyMetadata("AI_Assisted_Creation", "This assembly was fully created with the assistance of Generative AI (code, documentation and icon), directed by the mod author.")] [assembly: AssemblyTitle("RecipeSync")] [assembly: AssemblyMetadata("AI_Model_Vendor", "Anthropic")] [assembly: AssemblyMetadata("AI_Model", "Claude Opus 5.5")] [assembly: CompilationRelaxations(8)] [assembly: AssemblyVersion("1.0.0.0")] namespace RecipeSync; internal sealed class LogData { public readonly HashSet<string> Materials = new HashSet<string>(StringComparer.Ordinal); public readonly HashSet<string> Recipes = new HashSet<string>(StringComparer.Ordinal); public readonly Dictionary<string, int> Stations = new Dictionary<string, int>(StringComparer.Ordinal); public int Count => Materials.Count + Recipes.Count + Stations.Count; public bool IsEmpty => Count == 0; public void Clear() { Materials.Clear(); Recipes.Clear(); Stations.Clear(); } public static bool IsValidName(string name) { return !string.IsNullOrEmpty(name); } public bool AddMaterial(string name) { if (IsValidName(name)) { return Materials.Add(name); } return false; } public bool AddRecipe(string name) { if (IsValidName(name)) { return Recipes.Add(name); } return false; } public bool AddStation(string name, int level) { if (!IsValidName(name)) { return false; } if (Stations.TryGetValue(name, out var value) && value >= level) { return false; } Stations[name] = level; return true; } public int MergeFrom(LogData other, LogData added) { if (other == null) { return 0; } int num = 0; foreach (string material in other.Materials) { if (AddMaterial(material)) { num++; added?.AddMaterial(material); } } foreach (string recipe in other.Recipes) { if (AddRecipe(recipe)) { num++; added?.AddRecipe(recipe); } } foreach (KeyValuePair<string, int> station in other.Stations) { if (AddStation(station.Key, station.Value)) { num++; added?.AddStation(station.Key, station.Value); } } return num; } public LogData Except(LogData other) { LogData logData = new LogData(); foreach (string material in Materials) { if (other == null || !other.Materials.Contains(material)) { logData.Materials.Add(material); } } foreach (string recipe in Recipes) { if (other == null || !other.Recipes.Contains(recipe)) { logData.Recipes.Add(recipe); } } foreach (KeyValuePair<string, int> station in Stations) { if (other == null || !other.Stations.TryGetValue(station.Key, out var value) || value < station.Value) { logData.Stations[station.Key] = station.Value; } } return logData; } public LogData Copy() { LogData logData = new LogData(); logData.MergeFrom(this, null); return logData; } } internal sealed class PlayerAccess { private const BindingFlags Flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private readonly FieldInfo _materials; private readonly FieldInfo _recipes; private readonly FieldInfo _stations; private readonly MethodInfo _updateKnownRecipesList; private readonly MethodInfo _updateAvailablePiecesList; private readonly MethodInfo _updateEvents; private bool _refreshErrorLogged; public readonly bool IsValid; public PlayerAccess() { Type typeFromHandle = typeof(Player); _materials = FindField(typeFromHandle, "m_knownMaterial", typeof(HashSet<string>)); _recipes = FindField(typeFromHandle, "m_knownRecipes", typeof(HashSet<string>)); _stations = FindField(typeFromHandle, "m_knownStations", typeof(Dictionary<string, int>)); _updateKnownRecipesList = FindMethod(typeFromHandle, "UpdateKnownRecipesList"); _updateAvailablePiecesList = FindMethod(typeFromHandle, "UpdateAvailablePiecesList"); _updateEvents = FindMethod(typeFromHandle, "UpdateEvents"); IsValid = _materials != null && _recipes != null && _stations != null; } public HashSet<string> Materials(Player p) { return (HashSet<string>)_materials.GetValue(p); } public HashSet<string> Recipes(Player p) { return (HashSet<string>)_recipes.GetValue(p); } public Dictionary<string, int> Stations(Player p) { return (Dictionary<string, int>)_stations.GetValue(p); } public void Collect(Player p, LogData known, LogData into, bool materials, bool recipes, bool stations) { if (materials) { HashSet<string> hashSet = Materials(p); if (hashSet != null) { foreach (string item in hashSet) { if (known == null || !known.Materials.Contains(item)) { into.AddMaterial(item); } } } } if (recipes) { HashSet<string> hashSet2 = Recipes(p); if (hashSet2 != null) { foreach (string item2 in hashSet2) { if (known == null || !known.Recipes.Contains(item2)) { into.AddRecipe(item2); } } } } if (!stations) { return; } Dictionary<string, int> dictionary = Stations(p); if (dictionary == null) { return; } foreach (KeyValuePair<string, int> item3 in dictionary) { if (known == null || !known.Stations.TryGetValue(item3.Key, out var value) || value < item3.Value) { into.AddStation(item3.Key, item3.Value); } } } public void Refresh(Player p) { Invoke(_updateKnownRecipesList, p); Invoke(_updateAvailablePiecesList, p); Invoke(_updateEvents, p); } public string DescribeMissing() { List<string> list = new List<string>(); if (_materials == null) { list.Add("Player.m_knownMaterial"); } if (_recipes == null) { list.Add("Player.m_knownRecipes"); } if (_stations == null) { list.Add("Player.m_knownStations"); } if (_updateKnownRecipesList == null) { list.Add("Player.UpdateKnownRecipesList()"); } if (_updateAvailablePiecesList == null) { list.Add("Player.UpdateAvailablePiecesList()"); } if (_updateEvents == null) { list.Add("Player.UpdateEvents()"); } return string.Join(", ", list.ToArray()); } private void Invoke(MethodInfo method, Player p) { if (method == null) { return; } try { method.Invoke(p, null); } catch (Exception ex) { if (!_refreshErrorLogged) { _refreshErrorLogged = true; Exception ex2 = ((ex is TargetInvocationException && ex.InnerException != null) ? ex.InnerException : ex); RecipeSyncPlugin.Log.LogWarning((object)("Player." + method.Name + "() threw after a sync (entries were still added): " + ex2)); } } } private static FieldInfo FindField(Type t, string name, Type expected) { FieldInfo field = t.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (!(field != null) || !expected.IsAssignableFrom(field.FieldType)) { return null; } return field; } private static MethodInfo FindMethod(Type t, string name) { return t.GetMethod(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null); } } [BepInPlugin("Rock3t.RecipeSync", "RecipeSync", "1.0.0")] public sealed class RecipeSyncPlugin : BaseUnityPlugin { public const string Guid = "Rock3t.RecipeSync"; public const string ModName = "RecipeSync"; public const string ModVersion = "1.0.0"; private const float TickInterval = 1f; internal static ManualLogSource Log; private ConfigEntry<bool> _syncMaterials; private ConfigEntry<bool> _syncRecipes; private ConfigEntry<bool> _syncStations; private ConfigEntry<bool> _showNotifications; private PlayerAccess _access; private string _lastErrorMessage; private ZRoutedRpc _registeredRpc; private ServerStore _store; private readonly LogData _shared = new LogData(); private readonly Dictionary<long, LogData> _joinSnapshots = new Dictionary<long, LogData>(); private Player _lastPlayer; private bool _announced; private bool _serverHasMod; private bool _reapply; private float _nextTick; private void Awake() { Log = ((BaseUnityPlugin)this).Logger; _syncMaterials = ((BaseUnityPlugin)this).Config.Bind<bool>("Sync", "Items", true, "Share which items/materials have been picked up. This is what unlocks most recipes."); _syncRecipes = ((BaseUnityPlugin)this).Config.Bind<bool>("Sync", "RecipesAndPieces", true, "Share known crafting recipes and build pieces."); _syncStations = ((BaseUnityPlugin)this).Config.Bind<bool>("Sync", "CraftingStations", true, "Share which crafting stations (and upgrade levels) have been discovered. Some recipes only unlock after the required station level has been seen."); _showNotifications = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "ShowNotifications", true, "Show a message in the top-left corner when entries from other players are added to your log."); EventHandler eventHandler = delegate { _reapply = true; }; _syncMaterials.SettingChanged += eventHandler; _syncRecipes.SettingChanged += eventHandler; _syncStations.SettingChanged += eventHandler; _access = new PlayerAccess(); if (!_access.IsValid) { Log.LogError((object)("This game version's Player class is missing " + _access.DescribeMissing() + ". Player logs won't be synced, but a server will still keep the shared log.")); } Log.LogInfo((object)"RecipeSync 1.0.0 loaded."); } private void Update() { try { Tick(); } catch (Exception e) { LogErrorOnce("Update", e); } } private void OnApplicationQuit() { SaveStore(); } private void OnDestroy() { SaveStore(); } private void Tick() { ZNet instance = ZNet.instance; ZRoutedRpc instance2 = ZRoutedRpc.instance; if ((Object)(object)instance == (Object)null || instance2 == null) { if (_registeredRpc != null) { EndSession(); } return; } if (instance2 != _registeredRpc) { if (_registeredRpc != null) { EndSession(); } BeginSession(instance2); } float unscaledTime = Time.unscaledTime; if (!(unscaledTime < _nextTick)) { _nextTick = unscaledTime + 1f; if (instance.IsServer()) { EnsureStore(instance)?.SaveIfDue(unscaledTime); } if (!instance.IsDedicated()) { ClientTick(); } } } private void BeginSession(ZRoutedRpc rpc) { ResetSessionState(); rpc.Register<ZPackage>("RecipeSync_Data", (Action<long, ZPackage>)OnData); rpc.Register<ZPackage>("RecipeSync_Hello", (Action<long, ZPackage>)OnHello); _registeredRpc = rpc; Log.LogDebug((object)"Registered sync RPCs for new session."); } private void EndSession() { SaveStore(); ResetSessionState(); } private void ResetSessionState() { _registeredRpc = null; _store = null; _shared.Clear(); _joinSnapshots.Clear(); _lastPlayer = null; _announced = false; _serverHasMod = false; _reapply = false; _nextTick = 0f; } private void SaveStore() { if (_store == null) { return; } try { _store.SaveIfDirty(); } catch (Exception e) { LogErrorOnce("Save", e); } } private ServerStore EnsureStore(ZNet znet) { if (_store != null || (Object)(object)znet == (Object)null || !znet.IsServer()) { return _store; } string worldName = znet.GetWorldName(); if (string.IsNullOrEmpty(worldName)) { return null; } _store = ServerStore.Load(worldName, znet.GetWorldUID()); return _store; } private void ClientTick() { if (!_access.IsValid) { return; } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { return; } if ((Object)(object)localPlayer != (Object)(object)_lastPlayer) { _lastPlayer = localPlayer; _reapply = true; } if (_reapply) { _reapply = false; ApplyToPlayer(localPlayer, _shared); } if (!_announced) { LogData logData = new LogData(); _access.Collect(localPlayer, null, logData, _syncMaterials.Value, _syncRecipes.Value, _syncStations.Value); _shared.MergeFrom(logData, null); SendData(0L, logData, 2, alwaysSendOne: false); ZRoutedRpc.instance.InvokeRoutedRPC(0L, "RecipeSync_Hello", new object[1] { Protocol.BuildHello() }); _announced = true; Log.LogInfo((object)("Shared this character's log (" + logData.Count + " entries) and requested the world's shared log.")); } else { LogData logData2 = new LogData(); _access.Collect(localPlayer, _shared, logData2, _syncMaterials.Value, _syncRecipes.Value, _syncStations.Value); if (!logData2.IsEmpty) { _shared.MergeFrom(logData2, null); SendData(0L, logData2, 0, alwaysSendOne: false); Log.LogDebug((object)("Shared " + logData2.Count + " new log entries.")); } } } private void ReceiveShared(LogData incoming) { LogData logData = new LogData(); _shared.MergeFrom(incoming, logData); if (!logData.IsEmpty && _access.IsValid) { Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null) { ApplyToPlayer(localPlayer, logData); } } } private void ApplyToPlayer(Player player, LogData data) { if (data.IsEmpty) { return; } int num = 0; int num2 = 0; int num3 = 0; if (_syncMaterials.Value) { HashSet<string> hashSet = _access.Materials(player); if (hashSet != null) { foreach (string material in data.Materials) { if (hashSet.Add(material)) { num++; } } } } if (_syncRecipes.Value) { HashSet<string> hashSet2 = _access.Recipes(player); if (hashSet2 != null) { foreach (string recipe in data.Recipes) { if (hashSet2.Add(recipe)) { num2++; } } } } if (_syncStations.Value) { Dictionary<string, int> dictionary = _access.Stations(player); if (dictionary != null) { foreach (KeyValuePair<string, int> station in data.Stations) { if (!dictionary.TryGetValue(station.Key, out var value) || value < station.Value) { dictionary[station.Key] = station.Value; num3++; } } } } if (num + num2 + num3 == 0) { return; } _access.Refresh(player); string text = Describe(num2, num, num3); Log.LogInfo((object)("Added from shared log: " + text + ".")); if (!_showNotifications.Value) { return; } try { ShowMessage(player, "Shared recipe log: +" + text); } catch (Exception e) { LogErrorOnce("Message", e); } } [MethodImpl(MethodImplOptions.NoInlining)] private static void ShowMessage(Player player, string text) { ((Character)player).Message((MessageType)1, text, 0, (Sprite)null, false); } private static string Describe(int recipes, int materials, int stations) { List<string> list = new List<string>(); if (recipes > 0) { list.Add(recipes + ((recipes == 1) ? " recipe/piece" : " recipes/pieces")); } if (materials > 0) { list.Add(materials + ((materials == 1) ? " item" : " items")); } if (stations > 0) { list.Add(stations + ((stations == 1) ? " station" : " stations")); } return string.Join(", ", list.ToArray()); } private void SendData(long target, LogData data, byte flags, bool alwaysSendOne) { ZRoutedRpc instance = ZRoutedRpc.instance; if (instance == null) { return; } foreach (ZPackage item in Protocol.BuildData(data, flags, alwaysSendOne)) { instance.InvokeRoutedRPC(target, "RecipeSync_Data", new object[1] { item }); } } private void OnData(long sender, ZPackage pkg) { try { if (!Protocol.TryReadData(pkg, out var flags, out var data)) { return; } ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { return; } if (instance.IsServer()) { ServerStore serverStore = EnsureStore(instance); if (serverStore != null) { int num = serverStore.Merge(data, Time.unscaledTime); if (num > 0) { Log.LogDebug((object)("Shared log for this world gained " + num + " entries.")); } } } if (sender == ZNet.GetUID()) { return; } if ((flags & 2) != 0) { if (!_joinSnapshots.TryGetValue(sender, out var value)) { value = new LogData(); _joinSnapshots[sender] = value; } value.MergeFrom(data, null); } if ((flags & 1) != 0) { _serverHasMod = true; } if (!instance.IsDedicated()) { ReceiveShared(data); } } catch (Exception e) { LogErrorOnce("OnData", e); } } private void OnHello(long sender, ZPackage pkg) { try { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { return; } bool flag = sender == ZNet.GetUID(); if (_joinSnapshots.TryGetValue(sender, out var value)) { _joinSnapshots.Remove(sender); } if (instance.IsServer()) { ServerStore serverStore = EnsureStore(instance); if (serverStore == null) { return; } if (flag) { if (!instance.IsDedicated()) { ReceiveShared(serverStore.Data); } return; } LogData logData = serverStore.Data.Except(value); SendData(sender, logData, 1, alwaysSendOne: true); Log.LogInfo((object)("Sent " + logData.Count + " shared log entries to joining player " + sender + ".")); } else if (!flag && !_serverHasMod && !instance.IsDedicated() && _announced) { LogData logData2 = _shared.Except(value); if (!logData2.IsEmpty) { SendData(sender, logData2, 0, alwaysSendOne: false); } } } catch (Exception e) { LogErrorOnce("OnHello", e); } } private void LogErrorOnce(string where, Exception e) { string text = where + ": " + e.Message; if (!(text == _lastErrorMessage)) { _lastErrorMessage = text; Log.LogError((object)("Error in " + where + " (sync keeps running): " + e)); } } } internal static class Protocol { public const string RpcData = "RecipeSync_Data"; public const string RpcHello = "RecipeSync_Hello"; public const int Version = 1; public const byte FlagFromServer = 1; public const byte FlagJoinSnapshot = 2; private const int MaxEntriesPerPackage = 250; public static List<ZPackage> BuildData(LogData data, byte flags, bool alwaysSendOne) { List<ZPackage> list = new List<ZPackage>(); List<string> list2 = new List<string>(); List<string> list3 = new List<string>(); List<KeyValuePair<string, int>> list4 = new List<KeyValuePair<string, int>>(); int pending = 0; foreach (string material in data.Materials) { list2.Add(material); if (++pending >= 250) { Flush(list, flags, list2, list3, list4, ref pending); } } foreach (string recipe in data.Recipes) { list3.Add(recipe); if (++pending >= 250) { Flush(list, flags, list2, list3, list4, ref pending); } } foreach (KeyValuePair<string, int> station in data.Stations) { list4.Add(station); if (++pending >= 250) { Flush(list, flags, list2, list3, list4, ref pending); } } if (pending > 0 || (alwaysSendOne && list.Count == 0)) { Flush(list, flags, list2, list3, list4, ref pending); } return list; } private static void Flush(List<ZPackage> packages, byte flags, List<string> materials, List<string> recipes, List<KeyValuePair<string, int>> stations, ref int pending) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(1); val.Write(flags); val.Write(materials.Count); foreach (string material in materials) { val.Write(material); } val.Write(recipes.Count); foreach (string recipe in recipes) { val.Write(recipe); } val.Write(stations.Count); foreach (KeyValuePair<string, int> station in stations) { val.Write(station.Key); val.Write(station.Value); } packages.Add(val); materials.Clear(); recipes.Clear(); stations.Clear(); pending = 0; } public static bool TryReadData(ZPackage pkg, out byte flags, out LogData data) { flags = 0; data = null; try { int num = pkg.ReadInt(); if (num < 1) { return false; } flags = pkg.ReadByte(); LogData logData = new LogData(); int num2 = ReadCount(pkg); for (int i = 0; i < num2; i++) { logData.AddMaterial(pkg.ReadString()); } num2 = ReadCount(pkg); for (int j = 0; j < num2; j++) { logData.AddRecipe(pkg.ReadString()); } num2 = ReadCount(pkg); for (int k = 0; k < num2; k++) { string name = pkg.ReadString(); int level = pkg.ReadInt(); logData.AddStation(name, level); } data = logData; return true; } catch (Exception ex) { RecipeSyncPlugin.Log.LogWarning((object)("Ignoring malformed sync message: " + ex.Message)); return false; } } public static ZPackage BuildHello() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(1); return val; } private static int ReadCount(ZPackage pkg) { int num = pkg.ReadInt(); if (num < 0 || num > pkg.Size() - pkg.GetPos()) { throw new InvalidOperationException("bad entry count " + num); } return num; } } internal sealed class ServerStore { private const float SaveDelaySeconds = 5f; private const float MaxSaveDelaySeconds = 60f; public readonly LogData Data = new LogData(); private readonly string _path; private readonly string _worldLabel; private bool _dirty; private float _dirtySince; private float _lastChange; public string Path => _path; private ServerStore(string path, string worldLabel) { _path = path; _worldLabel = worldLabel; } public static ServerStore Load(string worldName, long worldUid) { string path = System.IO.Path.Combine(Paths.ConfigPath, "RecipeSync"); string path2 = SanitizeFileName(worldName) + "_" + worldUid + ".txt"; ServerStore serverStore = new ServerStore(System.IO.Path.Combine(path, path2), worldName + " (" + worldUid + ")"); serverStore.ReadFile(serverStore._path); serverStore.ReadFile(serverStore._path + ".tmp"); serverStore.ReadFile(serverStore._path + ".bak"); return serverStore; } public int Merge(LogData incoming, float now) { int num = Data.MergeFrom(incoming, null); if (num > 0) { if (!_dirty) { _dirty = true; _dirtySince = now; } _lastChange = now; } return num; } public void SaveIfDue(float now) { if (_dirty && (now - _lastChange >= 5f || now - _dirtySince >= 60f)) { Save(); } } public void SaveIfDirty() { if (_dirty) { Save(); } } private void Save() { try { Directory.CreateDirectory(System.IO.Path.GetDirectoryName(_path)); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("# RecipeSync shared discovery log for world: ").Append(Escape(_worldLabel)).Append('\n'); stringBuilder.Append("# Entries are only ever added. M = material/item, R = recipe/build piece, S = crafting station + level.\n"); foreach (string item in Sorted(Data.Materials)) { stringBuilder.Append("M\t").Append(Escape(item)).Append('\n'); } foreach (string item2 in Sorted(Data.Recipes)) { stringBuilder.Append("R\t").Append(Escape(item2)).Append('\n'); } List<string> list = new List<string>(Data.Stations.Keys); list.Sort(StringComparer.Ordinal); foreach (string item3 in list) { stringBuilder.Append("S\t").Append(Escape(item3)).Append('\t') .Append(Data.Stations[item3].ToString()) .Append('\n'); } string text = _path + ".tmp"; File.WriteAllText(text, stringBuilder.ToString(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); if (File.Exists(_path)) { File.Copy(_path, _path + ".bak", overwrite: true); File.Delete(_path); } File.Move(text, _path); _dirty = false; RecipeSyncPlugin.Log.LogDebug((object)("Saved shared log (" + Data.Count + " entries) to " + _path)); } catch (Exception ex) { RecipeSyncPlugin.Log.LogWarning((object)("Could not save shared log to " + _path + ": " + ex.Message)); _lastChange = (_dirtySince = Time.unscaledTime); } } private void ReadFile(string path) { try { if (!File.Exists(path)) { return; } string[] array = File.ReadAllLines(path, Encoding.UTF8); foreach (string text in array) { string text2 = text.TrimEnd('\r'); if (text2.Length < 3 || text2[0] == '#' || text2[1] != '\t') { continue; } string[] array2 = text2.Split('\t'); switch (text2[0]) { case 'M': Data.AddMaterial(Unescape(array2[1])); break; case 'R': Data.AddRecipe(Unescape(array2[1])); break; case 'S': { if (array2.Length >= 3 && int.TryParse(array2[2], out var result)) { Data.AddStation(Unescape(array2[1]), result); } break; } } } RecipeSyncPlugin.Log.LogInfo((object)("Loaded shared log for " + _worldLabel + " from " + path + " (" + Data.Count + " entries total)")); } catch (Exception ex) { RecipeSyncPlugin.Log.LogWarning((object)("Could not read " + path + ": " + ex.Message)); } } private static List<string> Sorted(HashSet<string> set) { List<string> list = new List<string>(set); list.Sort(StringComparer.Ordinal); return list; } private static string SanitizeFileName(string name) { if (string.IsNullOrEmpty(name)) { return "world"; } char[] invalidFileNameChars = System.IO.Path.GetInvalidFileNameChars(); StringBuilder stringBuilder = new StringBuilder(name.Length); foreach (char c in name) { stringBuilder.Append((Array.IndexOf(invalidFileNameChars, c) >= 0) ? '_' : c); } return stringBuilder.ToString(); } private static string Escape(string s) { return s.Replace("\\", "\\\\").Replace("\t", "\\t").Replace("\n", "\\n") .Replace("\r", "\\r"); } private static string Unescape(string s) { if (s.IndexOf('\\') < 0) { return s; } StringBuilder stringBuilder = new StringBuilder(s.Length); for (int i = 0; i < s.Length; i++) { char c = s[i]; if (c == '\\' && i + 1 < s.Length) { char c2 = s[++i]; switch (c2) { case 't': stringBuilder.Append('\t'); break; case 'n': stringBuilder.Append('\n'); break; case 'r': stringBuilder.Append('\r'); break; default: stringBuilder.Append(c2); break; } } else { stringBuilder.Append(c); } } return stringBuilder.ToString(); } }