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 SoundReplacer v1.6.0
SoundReplacer.dll
Decompiled 8 minutes agousing System; using System.Collections; 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.Versioning; using System.Text; using BepInEx; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using SoundReplacer.Audio; using SoundReplacer.Catalog; using SoundReplacer.Logging; using SoundReplacer.Shared; using UnityEngine; using UnityEngine.Networking; using UnityEngine.SceneManagement; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = "")] [assembly: AssemblyCompany("SoundReplacer")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("Runtime sound replacement for R.E.P.O. (BepInEx 5 / Harmony)")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+f98919c188327baa412aa3d267715bc6c1166480")] [assembly: AssemblyProduct("SoundReplacer")] [assembly: AssemblyTitle("SoundReplacer")] [assembly: AssemblyVersion("1.0.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 SoundReplacer { [BepInPlugin("com.soundreplacer.repo", "SoundReplacer", "1.6.0")] public sealed class Plugin : BaseUnityPlugin { public const string Guid = "com.soundreplacer.repo"; public const string PluginName = "SoundReplacer"; public const string PluginVersion = "1.6.0"; private ISoundBackend _backend; private ReplacerConfig _config; private bool _ready; internal static Plugin Instance { get; private set; } internal static string PluginDirectory { get; private set; } internal static string ConfigFilePath => Path.Combine(PluginDirectory, "config.json"); internal static string CatalogFilePath => Path.Combine(PluginDirectory, "catalog.json"); internal static string SoundsDirectory => Path.Combine(PluginDirectory, "Sounds"); internal static string LogFilePath => Path.Combine(PluginDirectory, "SoundReplacer.log"); internal SoundReplacementManager Replacements { get; private set; } internal CatalogCollector Catalog { get; private set; } private void Awake() { Instance = this; try { Initialize(); } catch (Exception ex) { _ready = false; ModLog.Error("инициализация SoundReplacer провалилась, мод отключён: " + ex.Message, ex); ((BaseUnityPlugin)this).Logger.LogError((object)("[SoundReplacer] " + ex)); } } private void Initialize() { //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Unknown result type (might be due to invalid IL or missing references) string text = Path.GetDirectoryName(((BaseUnityPlugin)this).Info.Location); if (string.IsNullOrEmpty(text)) { text = Paths.PluginPath; } PluginDirectory = text; Directory.CreateDirectory(PluginDirectory); Directory.CreateDirectory(SoundsDirectory); ModLog.Initialize(LogFilePath, ((BaseUnityPlugin)this).Logger); ModLog.Info("SoundReplacer 1.6.0 стартует из " + PluginDirectory); ModLog.Info("игра " + Application.version + ", Unity " + Application.unityVersion + ", платформа " + ((object)Application.platform/*cast due to .constrained prefix*/).ToString()); if (SoundApiProbe.IsLoaded(out var details)) { ModLog.Info("в процессе найдена SoundAPI-сборка (" + details + "), используется собственный Harmony-бэкенд"); } _config = LoadConfig(); Replacements = new SoundReplacementManager(_config, PluginDirectory); _backend = new HarmonyAudioBackend(); if (!_backend.Install(Replacements)) { ModLog.Error("Harmony-бэкенд не установлен, звуки останутся оригинальными"); } Catalog = new CatalogCollector(CatalogFilePath, _config.ExportCatalog); Catalog.ScanLoadedClips(); SceneManager.sceneLoaded += OnSceneLoaded; _ready = true; ((MonoBehaviour)this).StartCoroutine(StartupRoutine()); } private IEnumerator StartupRoutine() { yield return null; yield return null; if (_config != null) { if (!_config.Enabled) { ModLog.Warn("замена звуков отключена в config.json (enabled = false)"); yield break; } yield return Replacements.LoadAllCoroutine(); ModLog.Info("активных замен: " + Replacements.Count); Catalog.WriteNow(); } } private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { if (_ready) { Catalog.ScanLoadedClips(); } } private void Update() { if (_ready) { Catalog.Tick(); } } private void OnApplicationQuit() { if (_ready) { Catalog.WriteNow(); } } private void OnDestroy() { SceneManager.sceneLoaded -= OnSceneLoaded; try { if (Catalog != null) { Catalog.WriteNow(); } } catch (Exception ex) { ModLog.Warn("не удалось сохранить catalog.json при выходе: " + ex.Message); } if (_backend != null) { _backend.Uninstall(); } if (Replacements != null) { Replacements.UnloadAll(); } ModLog.Info("SoundReplacer выгружен"); ModLog.Shutdown(); } private ReplacerConfig LoadConfig() { string configFilePath = ConfigFilePath; if (!File.Exists(configFilePath)) { ModLog.Warn("config.json не найден: " + configFilePath); ModLog.Warn("пока звуки оригинальные. Запустите SoundReplacer.App, выберите звуки и нажмите «Применить»."); return new ReplacerConfig(); } if (!ReplacerConfig.TryLoad(configFilePath, out var config, out var error)) { ModLog.Error("config.json не прочитан: " + error); ModLog.Error("играют оригинальные звуки; исправьте файл и перезапустите игру"); return new ReplacerConfig(); } ModLog.Info("конфиг загружен: " + config.Describe()); return config; } } internal static class SoundApiProbe { private static readonly string[] Markers = new string[3] { "SoundAPI", "SoundApi", "Loaforc" }; internal static bool IsLoaded(out string details) { List<string> list = new List<string>(); try { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); for (int i = 0; i < assemblies.Length; i++) { string name = assemblies[i].GetName().Name; if (string.IsNullOrEmpty(name)) { continue; } for (int j = 0; j < Markers.Length; j++) { if (name.IndexOf(Markers[j], StringComparison.OrdinalIgnoreCase) >= 0) { list.Add(name); break; } } } } catch (Exception ex) { ModLog.Debug("SoundAPI probe: " + ex.Message); } details = ((list.Count == 0) ? null : string.Join(", ", list.ToArray())); return list.Count > 0; } } } namespace SoundReplacer.Shared { public static class Json { private sealed class Parser { private readonly string _text; private int _position; internal bool AtEnd => _position >= _text.Length; internal Parser(string text) { _text = text; } internal FormatException Error(string message) { return new FormatException("JSON, позиция " + _position + ": " + message); } internal void SkipWhitespace() { while (_position < _text.Length) { char c = _text[_position]; if (c == ' ' || c == '\t' || c == '\r' || c == '\n') { _position++; continue; } break; } } internal object ParseValue(int depth) { if (depth > 64) { throw Error("слишком большая вложенность"); } SkipWhitespace(); if (AtEnd) { throw Error("неожиданный конец документа"); } char c = _text[_position]; switch (c) { case '{': return ParseObject(depth); case '[': return ParseArray(depth); case '"': return ParseString(); case 't': ExpectLiteral("true"); return true; case 'f': ExpectLiteral("false"); return false; case 'n': ExpectLiteral("null"); return null; default: switch (c) { case '-': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': return ParseNumber(); default: throw Error("неожиданный символ '" + c + "'"); } } } private void ExpectLiteral(string literal) { if (_position + literal.Length > _text.Length || string.CompareOrdinal(_text, _position, literal, 0, literal.Length) != 0) { throw Error("ожидалось '" + literal + "'"); } _position += literal.Length; } private IDictionary<string, object> ParseObject(int depth) { _position++; Dictionary<string, object> dictionary = new Dictionary<string, object>(StringComparer.Ordinal); SkipWhitespace(); if (AtEnd) { throw Error("незакрытый объект"); } if (_text[_position] == '}') { _position++; return dictionary; } while (true) { SkipWhitespace(); if (AtEnd) { throw Error("незакрытый объект"); } if (_text[_position] != '"') { throw Error("ожидалось имя свойства в кавычках"); } string key = ParseString(); SkipWhitespace(); if (AtEnd || _text[_position] != ':') { throw Error("ожидалось ':'"); } _position++; object value = ParseValue(depth + 1); dictionary[key] = value; SkipWhitespace(); if (AtEnd) { break; } switch (_text[_position++]) { case ',': break; case '}': return dictionary; default: throw Error("ожидалось ',' или '}'"); } } throw Error("незакрытый объект"); } private IList<object> ParseArray(int depth) { _position++; List<object> list = new List<object>(); SkipWhitespace(); if (AtEnd) { throw Error("незакрытый массив"); } if (_text[_position] == ']') { _position++; return list; } while (true) { object item = ParseValue(depth + 1); list.Add(item); SkipWhitespace(); if (AtEnd) { break; } switch (_text[_position++]) { case ',': break; case ']': return list; default: throw Error("ожидалось ',' или ']'"); } } throw Error("незакрытый массив"); } private string ParseString() { _position++; StringBuilder stringBuilder = new StringBuilder(32); while (!AtEnd) { char c = _text[_position++]; switch (c) { case '"': return stringBuilder.ToString(); default: stringBuilder.Append(c); break; case '\\': { if (AtEnd) { throw Error("незакрытая escape-последовательность"); } char c2 = _text[_position++]; switch (c2) { case '"': stringBuilder.Append('"'); break; case '\\': stringBuilder.Append('\\'); break; case '/': stringBuilder.Append('/'); break; case 'b': stringBuilder.Append('\b'); break; case 'f': stringBuilder.Append('\f'); break; case 'n': stringBuilder.Append('\n'); break; case 'r': stringBuilder.Append('\r'); break; case 't': stringBuilder.Append('\t'); break; case 'u': { if (_position + 4 > _text.Length) { throw Error("некорректный \\u escape"); } int num = 0; for (int i = 0; i < 4; i++) { int num2 = HexValue(_text[_position++]); if (num2 < 0) { throw Error("некорректный \\u escape"); } num = (num << 4) | num2; } stringBuilder.Append((char)num); break; } default: throw Error("недопустимый escape '\\" + c2 + "'"); } break; } } } throw Error("незакрытая строка"); } private object ParseNumber() { int position = _position; if (_text[_position] == '-') { _position++; } while (_position < _text.Length && char.IsDigit(_text[_position])) { _position++; } if (_position < _text.Length && _text[_position] == '.') { _position++; while (_position < _text.Length && char.IsDigit(_text[_position])) { _position++; } } if (_position < _text.Length && (_text[_position] == 'e' || _text[_position] == 'E')) { _position++; if (_position < _text.Length && (_text[_position] == '+' || _text[_position] == '-')) { _position++; } while (_position < _text.Length && char.IsDigit(_text[_position])) { _position++; } } string text = _text.Substring(position, _position - position); if (!double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { throw Error("некорректное число '" + text + "'"); } return result; } private static int HexValue(char c) { if (c >= '0' && c <= '9') { return c - 48; } if (c >= 'a' && c <= 'f') { return c - 97 + 10; } if (c >= 'A' && c <= 'F') { return c - 65 + 10; } return -1; } } private const int MaxDepth = 64; public static object Parse(string text) { if (!TryParse(text, out var value, out var error)) { throw new FormatException(error); } return value; } public static bool TryParse(string text, out object value, out string error) { value = null; error = null; if (text == null) { error = "JSON: пустой ввод (null)"; return false; } try { Parser parser = new Parser(text); value = parser.ParseValue(0); parser.SkipWhitespace(); if (!parser.AtEnd) { throw parser.Error("лишние символы после конца документа"); } return true; } catch (FormatException ex) { error = ex.Message; return false; } } public static string Serialize(object value, bool pretty = true) { StringBuilder stringBuilder = new StringBuilder(512); WriteValue(stringBuilder, value, pretty, 0); if (pretty) { stringBuilder.Append('\n'); } return stringBuilder.ToString(); } public static IDictionary<string, object> AsObject(object value) { return value as IDictionary<string, object>; } public static IList<object> AsArray(object value) { return value as IList<object>; } public static object Get(IDictionary<string, object> obj, string key) { if (obj != null && obj.TryGetValue(key, out var value)) { return value; } return null; } public static string GetString(IDictionary<string, object> obj, string key, string fallback = null) { return (Get(obj, key) as string) ?? fallback; } public static bool GetBool(IDictionary<string, object> obj, string key, bool fallback) { object obj2 = Get(obj, key); if (obj2 is bool) { return (bool)obj2; } if (obj2 is string && bool.TryParse((string)obj2, out var result)) { return result; } return fallback; } public static int GetInt(IDictionary<string, object> obj, string key, int fallback) { object obj2 = Get(obj, key); if (obj2 == null) { return fallback; } try { return Convert.ToInt32(obj2, CultureInfo.InvariantCulture); } catch { return fallback; } } public static double GetDouble(IDictionary<string, object> obj, string key, double fallback) { object obj2 = Get(obj, key); if (obj2 == null) { return fallback; } try { return Convert.ToDouble(obj2, CultureInfo.InvariantCulture); } catch { return fallback; } } private static void WriteValue(StringBuilder sb, object value, bool pretty, int depth) { if (value == null) { sb.Append("null"); } else if (value is string) { WriteString(sb, (string)value); } else if (value is bool) { sb.Append(((bool)value) ? "true" : "false"); } else if (value is char) { WriteString(sb, value.ToString()); } else if (value is IDictionary<string, object> dict) { WriteObject(sb, dict, pretty, depth); } else if (value is IEnumerable items) { WriteArray(sb, items, pretty, depth); } else if (IsNumber(value)) { WriteNumber(sb, value); } else { WriteString(sb, Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty); } } private static bool IsNumber(object value) { if (!(value is sbyte) && !(value is byte) && !(value is short) && !(value is ushort) && !(value is int) && !(value is uint) && !(value is long) && !(value is ulong) && !(value is float) && !(value is double)) { return value is decimal; } return true; } private static void WriteNumber(StringBuilder sb, object value) { double num = Convert.ToDouble(value, CultureInfo.InvariantCulture); if (double.IsNaN(num) || double.IsInfinity(num)) { sb.Append("null"); } else if (num == Math.Floor(num) && Math.Abs(num) < 9000000000000000.0) { sb.Append(((long)num).ToString(CultureInfo.InvariantCulture)); } else { sb.Append(num.ToString("R", CultureInfo.InvariantCulture)); } } private static void WriteObject(StringBuilder sb, IDictionary<string, object> dict, bool pretty, int depth) { if (dict.Count == 0) { sb.Append("{}"); return; } sb.Append('{'); bool flag = true; foreach (KeyValuePair<string, object> item in dict) { if (!flag) { sb.Append(','); } flag = false; if (pretty) { sb.Append('\n'); AppendIndent(sb, depth + 1); } WriteString(sb, item.Key); sb.Append(pretty ? ": " : ":"); WriteValue(sb, item.Value, pretty, depth + 1); } if (pretty) { sb.Append('\n'); AppendIndent(sb, depth); } sb.Append('}'); } private static void WriteArray(StringBuilder sb, IEnumerable items, bool pretty, int depth) { StringBuilder stringBuilder = new StringBuilder(); int num = 0; foreach (object item in items) { if (num > 0) { stringBuilder.Append(','); } if (pretty) { stringBuilder.Append('\n'); AppendIndent(stringBuilder, depth + 1); } WriteValue(stringBuilder, item, pretty, depth + 1); num++; } if (num == 0) { sb.Append("[]"); return; } sb.Append('['); sb.Append(stringBuilder); if (pretty) { sb.Append('\n'); AppendIndent(sb, depth); } sb.Append(']'); } private static void AppendIndent(StringBuilder sb, int depth) { sb.Append(' ', depth * 2); } private static void WriteString(StringBuilder sb, string value) { sb.Append('"'); foreach (char c in value) { switch (c) { case '"': sb.Append("\\\""); continue; case '\\': sb.Append("\\\\"); continue; case '\b': sb.Append("\\b"); continue; case '\f': sb.Append("\\f"); continue; case '\n': sb.Append("\\n"); continue; case '\r': sb.Append("\\r"); continue; case '\t': sb.Append("\\t"); continue; } if (c < ' ') { sb.Append("\\u"); int num = c; sb.Append(num.ToString("x4", CultureInfo.InvariantCulture)); } else { sb.Append(c); } } sb.Append('"'); } } public sealed class ReplacerConfig { public const int CurrentVersion = 1; public const string FileName = "config.json"; public int Version { get; set; } public bool Enabled { get; set; } public bool PatchClipSetter { get; set; } public bool ExportCatalog { get; set; } public bool MatchLength { get; set; } public bool MatchVolume { get; set; } public bool MatchChannels { get; set; } public bool LogPlays { get; set; } public List<string> NoTrim { get; private set; } public bool OncePerFile { get; set; } public double OnceWindowSeconds { get; set; } public double FadeOutSeconds { get; set; } public List<string> Spatial { get; private set; } public double MinDistance { get; set; } public double MaxDistance { get; set; } public double TrackVolume { get; set; } public bool Echo { get; set; } public double EchoDelayMs { get; set; } public double EchoDecayRatio { get; set; } public double EchoWetMix { get; set; } public double EchoDryMix { get; set; } public Dictionary<string, string> Sounds { get; private set; } public ReplacerConfig() { Version = 1; Enabled = true; PatchClipSetter = true; ExportCatalog = true; MatchLength = true; MatchVolume = true; MatchChannels = true; LogPlays = false; NoTrim = new List<string>(); OncePerFile = false; OnceWindowSeconds = 2.0; FadeOutSeconds = 0.0; Spatial = new List<string>(); MinDistance = 3.0; MaxDistance = 40.0; TrackVolume = 1.0; Echo = false; EchoDelayMs = 500.0; EchoDecayRatio = 0.5; EchoWetMix = 0.5; EchoDryMix = 1.0; Sounds = new Dictionary<string, string>(StringComparer.Ordinal); } public string ToJson() { Dictionary<string, object> dictionary = new Dictionary<string, object>(StringComparer.Ordinal); dictionary["version"] = Version; dictionary["enabled"] = Enabled; dictionary["patchClipSetter"] = PatchClipSetter; dictionary["exportCatalog"] = ExportCatalog; dictionary["matchLength"] = MatchLength; dictionary["matchVolume"] = MatchVolume; dictionary["matchChannels"] = MatchChannels; dictionary["logPlays"] = LogPlays; dictionary["oncePerFile"] = OncePerFile; dictionary["onceWindowSeconds"] = OnceWindowSeconds; dictionary["fadeOutSeconds"] = FadeOutSeconds; dictionary["spatial"] = new List<object>(Spatial.Count); foreach (string item in Spatial) { ((List<object>)dictionary["spatial"]).Add(item); } dictionary["minDistance"] = MinDistance; dictionary["maxDistance"] = MaxDistance; dictionary["trackVolume"] = TrackVolume; dictionary["echo"] = Echo; dictionary["echoDelayMs"] = EchoDelayMs; dictionary["echoDecayRatio"] = EchoDecayRatio; dictionary["echoWetMix"] = EchoWetMix; dictionary["echoDryMix"] = EchoDryMix; List<object> list = new List<object>(NoTrim.Count); foreach (string item2 in NoTrim) { list.Add(item2); } dictionary["noTrim"] = list; Dictionary<string, object> dictionary2 = new Dictionary<string, object>(StringComparer.Ordinal); foreach (KeyValuePair<string, string> item3 in Sounds.OrderBy<KeyValuePair<string, string>, string>((KeyValuePair<string, string> p) => p.Key, StringComparer.Ordinal)) { dictionary2[item3.Key] = item3.Value; } dictionary["sounds"] = dictionary2; return Json.Serialize(dictionary); } public static ReplacerConfig FromJson(string json, out string error) { error = null; if (!Json.TryParse(json, out var value, out var error2)) { error = error2; return null; } IDictionary<string, object> dictionary = Json.AsObject(value); if (dictionary == null) { error = "корень config.json должен быть JSON-объектом"; return null; } ReplacerConfig replacerConfig = new ReplacerConfig(); replacerConfig.Version = Json.GetInt(dictionary, "version", 1); replacerConfig.Enabled = Json.GetBool(dictionary, "enabled", fallback: true); replacerConfig.PatchClipSetter = Json.GetBool(dictionary, "patchClipSetter", fallback: true); replacerConfig.ExportCatalog = Json.GetBool(dictionary, "exportCatalog", fallback: true); replacerConfig.MatchLength = Json.GetBool(dictionary, "matchLength", fallback: true); replacerConfig.MatchVolume = Json.GetBool(dictionary, "matchVolume", fallback: true); replacerConfig.MatchChannels = Json.GetBool(dictionary, "matchChannels", fallback: true); replacerConfig.LogPlays = Json.GetBool(dictionary, "logPlays", fallback: false); replacerConfig.OncePerFile = Json.GetBool(dictionary, "oncePerFile", fallback: false); replacerConfig.OnceWindowSeconds = Json.GetDouble(dictionary, "onceWindowSeconds", 2.0); replacerConfig.FadeOutSeconds = Json.GetDouble(dictionary, "fadeOutSeconds", 0.0); replacerConfig.MinDistance = Json.GetDouble(dictionary, "minDistance", 3.0); replacerConfig.MaxDistance = Json.GetDouble(dictionary, "maxDistance", 40.0); replacerConfig.TrackVolume = Json.GetDouble(dictionary, "trackVolume", 1.0); replacerConfig.Echo = Json.GetBool(dictionary, "echo", fallback: false); replacerConfig.EchoDelayMs = Json.GetDouble(dictionary, "echoDelayMs", 500.0); replacerConfig.EchoDecayRatio = Json.GetDouble(dictionary, "echoDecayRatio", 0.5); replacerConfig.EchoWetMix = Json.GetDouble(dictionary, "echoWetMix", 0.5); replacerConfig.EchoDryMix = Json.GetDouble(dictionary, "echoDryMix", 1.0); IList<object> list = Json.AsArray(Json.Get(dictionary, "noTrim")); if (list != null) { foreach (object item in list) { string text = item as string; if (!string.IsNullOrEmpty(text)) { replacerConfig.NoTrim.Add(text.Trim()); } } } IList<object> list2 = Json.AsArray(Json.Get(dictionary, "spatial")); if (list2 != null) { foreach (object item2 in list2) { string text2 = item2 as string; if (!string.IsNullOrEmpty(text2)) { replacerConfig.Spatial.Add(text2.Trim()); } } } object obj = Json.Get(dictionary, "sounds"); if (obj != null) { IDictionary<string, object> dictionary2 = Json.AsObject(obj); if (dictionary2 == null) { error = "поле \"sounds\" должно быть объектом { \"имя\": \"путь\" }"; return null; } foreach (KeyValuePair<string, object> item3 in dictionary2) { string text3 = ((item3.Key == null) ? null : item3.Key.Trim()); string text4 = item3.Value as string; if (!string.IsNullOrEmpty(text3) && !string.IsNullOrEmpty(text4)) { replacerConfig.Sounds[text3] = text4.Trim(); } } } if (replacerConfig.Version > 1) { error = null; } return replacerConfig; } public static bool TryLoad(string path, out ReplacerConfig config, out string error) { config = null; error = null; if (!File.Exists(path)) { error = "файл не найден: " + path; return false; } try { string json = File.ReadAllText(path, Encoding.UTF8); config = FromJson(json, out error); return config != null; } catch (Exception ex) { error = ex.Message; return false; } } public bool Save(string path, out string error) { error = null; try { string directoryName = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(directoryName)) { Directory.CreateDirectory(directoryName); } string text = path + ".tmp"; File.WriteAllText(text, ToJson(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); if (File.Exists(path)) { try { File.Replace(text, path, null); } catch (Exception) { File.Delete(path); File.Move(text, path); } } else { File.Move(text, path); } return true; } catch (Exception ex2) { error = ex2.Message; return false; } } public static bool TryResolvePath(string pluginDirectory, string configValue, out string absolutePath, out string error) { absolutePath = null; error = null; if (string.IsNullOrEmpty(configValue)) { error = "пустой путь"; return false; } try { string text = configValue.Trim().Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar); absolutePath = (Path.IsPathRooted(text) ? Path.GetFullPath(text) : Path.GetFullPath(Path.Combine(pluginDirectory, text))); if (!File.Exists(absolutePath)) { error = "файл не найден: " + absolutePath; return false; } return true; } catch (Exception ex) { error = ex.Message; return false; } } public string Describe() { return string.Format(CultureInfo.InvariantCulture, "version={0}, enabled={1}, patchClipSetter={2}, matchLength={3}, matchVolume={4}, matchChannels={5}, logPlays={6}, noTrim={7}, oncePerFile={8}, fadeOut={9}, spatial={10}, sounds={11}", Version, Enabled, PatchClipSetter, MatchLength, MatchVolume, MatchChannels, LogPlays, NoTrim.Count, OncePerFile, FadeOutSeconds, Spatial.Count, Sounds.Count); } } public sealed class SoundCatalog { public const int CurrentVersion = 1; public const string FileName = "catalog.json"; public int Version { get; set; } public string GeneratedUtc { get; set; } public string GameVersion { get; set; } public string UnityVersion { get; set; } public string ModVersion { get; set; } public List<SoundCatalogEntry> Sounds { get; private set; } public SoundCatalog() { Version = 1; Sounds = new List<SoundCatalogEntry>(); } public string ToJson() { Dictionary<string, object> dictionary = new Dictionary<string, object>(StringComparer.Ordinal); dictionary["version"] = Version; dictionary["generatedUtc"] = GeneratedUtc ?? DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture); dictionary["gameVersion"] = GameVersion ?? string.Empty; dictionary["unityVersion"] = UnityVersion ?? string.Empty; dictionary["modVersion"] = ModVersion ?? string.Empty; List<object> list = new List<object>(Sounds.Count); foreach (SoundCatalogEntry sound in Sounds) { Dictionary<string, object> dictionary2 = new Dictionary<string, object>(StringComparer.Ordinal); dictionary2["name"] = sound.Name; dictionary2["plays"] = sound.Plays; dictionary2["length"] = sound.Length; dictionary2["channels"] = sound.Channels; dictionary2["lastSeenUtc"] = sound.LastSeenUtc ?? string.Empty; list.Add(dictionary2); } dictionary["sounds"] = list; return Json.Serialize(dictionary); } public static SoundCatalog FromJson(string json, out string error) { error = null; if (!Json.TryParse(json, out var value, out var error2)) { error = error2; return null; } IDictionary<string, object> dictionary = Json.AsObject(value); if (dictionary == null) { error = "корень catalog.json должен быть JSON-объектом"; return null; } SoundCatalog soundCatalog = new SoundCatalog(); soundCatalog.Version = Json.GetInt(dictionary, "version", 1); soundCatalog.GeneratedUtc = Json.GetString(dictionary, "generatedUtc", string.Empty); soundCatalog.GameVersion = Json.GetString(dictionary, "gameVersion", string.Empty); soundCatalog.UnityVersion = Json.GetString(dictionary, "unityVersion", string.Empty); soundCatalog.ModVersion = Json.GetString(dictionary, "modVersion", string.Empty); IList<object> list = Json.AsArray(Json.Get(dictionary, "sounds")); if (list != null) { foreach (object item in list) { IDictionary<string, object> dictionary2 = Json.AsObject(item); if (dictionary2 != null) { string text = Json.GetString(dictionary2, "name"); if (!string.IsNullOrEmpty(text)) { soundCatalog.Sounds.Add(new SoundCatalogEntry { Name = text, Plays = Json.GetInt(dictionary2, "plays", 0), Length = (float)Json.GetDouble(dictionary2, "length", 0.0), Channels = Json.GetInt(dictionary2, "channels", 0), LastSeenUtc = Json.GetString(dictionary2, "lastSeenUtc", string.Empty) }); } } } } soundCatalog.Sounds = soundCatalog.Sounds.OrderByDescending((SoundCatalogEntry s) => s.Plays).ThenBy<SoundCatalogEntry, string>((SoundCatalogEntry s) => s.Name, StringComparer.OrdinalIgnoreCase).ToList(); return soundCatalog; } public static bool TryLoad(string path, out SoundCatalog catalog, out string error) { catalog = null; error = null; if (!File.Exists(path)) { error = "файл не найден: " + path; return false; } try { catalog = FromJson(File.ReadAllText(path, Encoding.UTF8), out error); return catalog != null; } catch (Exception ex) { error = ex.Message; return false; } } public bool Save(string path, out string error) { error = null; try { string directoryName = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(directoryName)) { Directory.CreateDirectory(directoryName); } string text = path + ".tmp"; File.WriteAllText(text, ToJson(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); if (File.Exists(path)) { try { File.Replace(text, path, null); } catch (Exception) { File.Delete(path); File.Move(text, path); } } else { File.Move(text, path); } return true; } catch (Exception ex2) { error = ex2.Message; return false; } } } public sealed class SoundCatalogEntry { public string Name { get; set; } public int Plays { get; set; } public float Length { get; set; } public int Channels { get; set; } public string LastSeenUtc { get; set; } } } namespace SoundReplacer.Logging { public enum ModLogLevel { Debug, Info, Warn, Error } public static class ModLog { private const long MaxFileBytes = 4194304L; private static readonly object Sync = new object(); private static string _filePath; private static StreamWriter _writer; private static ManualLogSource _bepInEx; private static ModLogLevel _minimumLevel = ModLogLevel.Info; public static void Initialize(string filePath, ManualLogSource bepInExLogger) { lock (Sync) { _filePath = filePath; _bepInEx = bepInExLogger; try { string directoryName = Path.GetDirectoryName(filePath); if (!string.IsNullOrEmpty(directoryName)) { Directory.CreateDirectory(directoryName); } OpenWriterLocked(); } catch (Exception ex) { _writer = null; if (_bepInEx != null) { _bepInEx.LogWarning((object)("[SoundReplacer] не удалось открыть SoundReplacer.log: " + ex.Message)); } } } } public static void SetMinimumLevel(ModLogLevel level) { lock (Sync) { _minimumLevel = level; } } public static void Debug(string message) { Write(ModLogLevel.Debug, message, null); } public static void Info(string message, Exception exception = null) { Write(ModLogLevel.Info, message, exception); } public static void Warn(string message, Exception exception = null) { Write(ModLogLevel.Warn, message, exception); } public static void Error(string message, Exception exception = null) { Write(ModLogLevel.Error, message, exception); } public static void Shutdown() { lock (Sync) { try { if (_writer != null) { _writer.Flush(); _writer.Dispose(); } } catch { } finally { _writer = null; } } } private static void Write(ModLogLevel level, string message, Exception exception) { if (string.IsNullOrEmpty(message) && exception == null) { return; } string text = string.Format(CultureInfo.InvariantCulture, "{0} [{1,-5}] {2}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture), level.ToString().ToUpperInvariant(), message ?? string.Empty); if (exception != null) { text = text + Environment.NewLine + " " + exception.GetType().Name + ": " + exception.Message + Environment.NewLine + exception.StackTrace; } lock (Sync) { if (level < _minimumLevel) { return; } try { if (_writer == null && !string.IsNullOrEmpty(_filePath)) { OpenWriterLocked(); } if (_writer != null) { RotateIfNeededLocked(); _writer.WriteLine(text); } } catch { } WriteToBepInExLocked(level, message ?? string.Empty); } } private static void WriteToBepInExLocked(ModLogLevel level, string message) { if (_bepInEx == null) { return; } try { switch (level) { case ModLogLevel.Debug: _bepInEx.Log((LogLevel)32, (object)message); break; case ModLogLevel.Warn: _bepInEx.Log((LogLevel)4, (object)message); break; case ModLogLevel.Error: _bepInEx.Log((LogLevel)2, (object)message); break; default: _bepInEx.Log((LogLevel)16, (object)message); break; } } catch { } } private static void OpenWriterLocked() { if (_writer != null) { try { _writer.Dispose(); } catch { } _writer = null; } _writer = new StreamWriter(new FileStream(_filePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)) { AutoFlush = true }; } private static void RotateIfNeededLocked() { if (_writer == null || _writer.BaseStream.Length < 4194304) { return; } try { _writer.Dispose(); } catch { } _writer = null; string text = _filePath + ".1"; try { if (File.Exists(text)) { File.Delete(text); } File.Move(_filePath, text); } catch { } OpenWriterLocked(); } } } namespace SoundReplacer.Catalog { public sealed class CatalogCollector { private sealed class Entry { public int Plays; public float Length; public int Channels; public DateTime LastSeenUtc; } private const int MaxEntries = 20000; private readonly string _path; private readonly bool _enabled; private readonly object _sync = new object(); private readonly Dictionary<string, Entry> _entries = new Dictionary<string, Entry>(StringComparer.Ordinal); private DateTime _nextWriteUtc = DateTime.UtcNow.AddSeconds(5.0); private DateTime _nextScanUtc = DateTime.MinValue; private bool _dirty; private bool _limitLogged; public int Count { get { lock (_sync) { return _entries.Count; } } } public CatalogCollector(string path, bool enabled) { _path = path; _enabled = enabled; } public void Observe(AudioClip clip) { if (!_enabled || (Object)(object)clip == (Object)null) { return; } string name = ((Object)clip).name; if (string.IsNullOrEmpty(name)) { return; } lock (_sync) { if (!_entries.TryGetValue(name, out var value)) { if (_entries.Count >= 20000) { if (!_limitLogged) { _limitLogged = true; ModLog.Warn("каталог достиг лимита " + 20000 + " имён, новые имена игнорируются"); } return; } value = new Entry(); _entries[name] = value; } value.Plays++; value.LastSeenUtc = DateTime.UtcNow; if (value.Length <= 0f) { value.Length = clip.length; value.Channels = clip.channels; } _dirty = true; } } public void ScanLoadedClips() { if (!_enabled) { return; } DateTime utcNow = DateTime.UtcNow; if (utcNow < _nextScanUtc) { return; } _nextScanUtc = utcNow.AddSeconds(10.0); try { AudioClip[] array = Resources.FindObjectsOfTypeAll<AudioClip>(); int num = 0; AudioClip[] array2 = array; foreach (AudioClip val in array2) { if (!((Object)(object)val == (Object)null)) { string name = ((Object)val).name; if (!string.IsNullOrEmpty(name) && Register(name, val)) { num++; } } } if (num > 0) { ModLog.Info("каталог: добавлено " + num + " новых имён, всего " + Count); } } catch (Exception ex) { ModLog.Warn("сканирование AudioClip не удалось: " + ex.Message); } } public void Tick() { if (_enabled && !(DateTime.UtcNow < _nextWriteUtc)) { _nextWriteUtc = DateTime.UtcNow.AddSeconds(10.0); WriteNow(); } } public void WriteNow() { if (!_enabled) { return; } SoundCatalog soundCatalog; lock (_sync) { if (_entries.Count == 0 || (!_dirty && File.Exists(_path))) { return; } _dirty = false; soundCatalog = BuildLocked(); } if (!soundCatalog.Save(_path, out var error)) { ModLog.Warn("не удалось записать catalog.json: " + error); lock (_sync) { _dirty = true; return; } } ModLog.Debug("catalog.json обновлён: " + soundCatalog.Sounds.Count + " записей"); } private bool Register(string name, AudioClip clip) { lock (_sync) { if (_entries.ContainsKey(name)) { return false; } if (_entries.Count >= 20000) { return false; } _entries[name] = new Entry { Length = (((Object)(object)clip == (Object)null) ? 0f : clip.length), Channels = ((!((Object)(object)clip == (Object)null)) ? clip.channels : 0), LastSeenUtc = DateTime.UtcNow }; _dirty = true; return true; } } private SoundCatalog BuildLocked() { SoundCatalog soundCatalog = new SoundCatalog { GeneratedUtc = DateTime.UtcNow.ToString("o"), GameVersion = Application.version, UnityVersion = Application.unityVersion, ModVersion = "1.6.0" }; foreach (KeyValuePair<string, Entry> item in _entries.OrderByDescending((KeyValuePair<string, Entry> pair) => pair.Value.Plays).ThenBy<KeyValuePair<string, Entry>, string>((KeyValuePair<string, Entry> pair) => pair.Key, StringComparer.OrdinalIgnoreCase)) { soundCatalog.Sounds.Add(new SoundCatalogEntry { Name = item.Key, Plays = item.Value.Plays, Length = item.Value.Length, Channels = item.Value.Channels, LastSeenUtc = item.Value.LastSeenUtc.ToString("o") }); } return soundCatalog; } } } namespace SoundReplacer.Audio { public static class AudioFileLoader { public const int MaxDurationSeconds = 120; public const long MaxFileBytes = 209715200L; public static bool IsSupportedExtension(string path) { string extension = Path.GetExtension(path); if (string.IsNullOrEmpty(extension)) { return false; } extension = extension.ToLowerInvariant(); if (!(extension == ".wav")) { return extension == ".ogg"; } return true; } public static AudioClip LoadWav(string path) { FileInfo fileInfo = new FileInfo(path); if (!fileInfo.Exists) { throw new InvalidDataException("файл не найден"); } if (fileInfo.Length > 209715200) { throw new InvalidDataException("файл больше 200 МБ"); } byte[] array = File.ReadAllBytes(path); if (array.Length < 44) { throw new InvalidDataException("файл короче 44 байт — это не WAV"); } if (array[0] != 82 || array[1] != 73 || array[2] != 70 || array[3] != 70) { throw new InvalidDataException("нет сигнатуры RIFF"); } if (array[8] != 87 || array[9] != 65 || array[10] != 86 || array[11] != 69) { throw new InvalidDataException("нет сигнатуры WAVE"); } int num = 0; int num2 = 0; int num3 = 0; int num4 = 0; int num5 = -1; int num6 = 0; int num7 = 12; while (num7 + 8 <= array.Length) { string text = Encoding.ASCII.GetString(array, num7, 4); int num8 = BitConverter.ToInt32(array, num7 + 4); int num9 = num7 + 8; if (num8 < 0 || num9 > array.Length) { break; } if (text == "fmt ") { if (num8 < 16 || num9 + 16 > array.Length) { throw new InvalidDataException("некорректный fmt-чанк"); } num = BitConverter.ToUInt16(array, num9); num2 = BitConverter.ToUInt16(array, num9 + 2); num3 = (int)BitConverter.ToUInt32(array, num9 + 4); num4 = BitConverter.ToUInt16(array, num9 + 14); if (num == 65534 && num8 >= 40 && num9 + 26 <= array.Length) { num = BitConverter.ToUInt16(array, num9 + 24); } } else if (text == "data") { num5 = num9; num6 = Math.Min(num8, array.Length - num9); } long num10 = (long)num9 + (long)num8 + (num8 & 1); if (num10 <= num7) { break; } num7 = (int)Math.Min(num10, array.Length); } if (num != 1 && num != 3) { throw new InvalidDataException("поддерживается только PCM или IEEE-float WAV (format=" + num + ")"); } if (num2 < 1 || num2 > 2) { throw new InvalidDataException("поддерживается 1 или 2 канала, найдено " + num2); } if (num3 < 4000 || num3 > 192000) { throw new InvalidDataException("неподдерживаемая частота дискретизации: " + num3 + " Гц"); } if (num4 != 8 && num4 != 16 && num4 != 24 && num4 != 32) { throw new InvalidDataException("неподдерживаемая разрядность: " + num4 + " бит"); } if (num == 3 && num4 != 32) { throw new InvalidDataException("IEEE-float WAV поддерживается только с 32 битами"); } if (num5 < 0 || num6 <= 0) { throw new InvalidDataException("в файле нет data-чанка со звуком"); } int num11 = num4 / 8; int num12 = num11 * num2; int num13 = num6 / num12; if (num13 <= 0) { throw new InvalidDataException("в файле нет сэмплов"); } if (num13 > num3 * 120) { throw new InvalidDataException("звук длиннее " + 120 + " секунд"); } float[] array2 = new float[num13 * num2]; int num14 = num5; for (int i = 0; i < array2.Length; i++) { if (num == 3) { array2[i] = Clamp(BitConverter.ToSingle(array, num14)); } else { switch (num4) { case 8: array2[i] = (float)(array[num14] - 128) / 128f; break; case 16: array2[i] = (float)BitConverter.ToInt16(array, num14) / 32768f; break; case 24: { int num15 = array[num14] | (array[num14 + 1] << 8) | (array[num14 + 2] << 16); if ((num15 & 0x800000) != 0) { num15 |= -16777216; } array2[i] = (float)num15 / 8388608f; break; } default: array2[i] = (float)BitConverter.ToInt32(array, num14) / 2.1474836E+09f; break; } } num14 += num11; } AudioClip obj = AudioClip.Create(Path.GetFileNameWithoutExtension(path), num13, num2, num3, false); if ((Object)(object)obj == (Object)null) { throw new InvalidDataException("Unity не смог создать AudioClip (возможно, закончилась память)"); } if (!obj.SetData(array2, 0)) { throw new InvalidDataException("Unity не принял аудиоданные (SetData вернул false)"); } return obj; } private static float Clamp(float value) { if (value > 1f) { return 1f; } if (value < -1f) { return -1f; } return value; } internal static void LogFailure(string originalName, string file, string reason) { ModLog.Warn("звук '" + originalName + "' не заменён: " + reason + " (файл: " + file + ")"); } } internal static class ClipFitter { private const float PeakFloor = 0.0001f; private const float GainTolerance = 0.02f; public static AudioClip Fit(AudioClip original, AudioClip custom, bool matchLength, bool matchVolume, bool matchChannels, double fadeOutSeconds) { if ((Object)(object)original == (Object)null || (Object)(object)custom == (Object)null) { return null; } try { int channels = custom.channels; int samples = custom.samples; if (samples <= 0 || channels <= 0) { return null; } bool flag = matchChannels && original.channels == 1 && channels == 2; int num = samples; bool flag2 = false; if (matchLength && original.length > 0f && custom.length > original.length + 0.01f) { int num2 = (int)Math.Floor(original.length * (float)custom.frequency); if (num2 > 0 && num2 < samples) { num = num2; flag2 = true; } } float[] array = new float[custom.samples * channels]; if (!custom.GetData(array, 0)) { ModLog.Warn("подгонка '" + ((Object)original).name + "': Unity не отдал сэмплы файла, оставляю как есть"); return null; } float num3 = Peak(array, array.Length); float num4 = 1f; bool flag3 = false; if (matchVolume && num3 > 0.0001f) { float num5 = TryPeak(original); if (num5 > 0.0001f) { num4 = num5 / num3; if (Math.Abs(num4 - 1f) > 0.02f) { flag3 = true; } else { num4 = 1f; } } } if (!flag2 && !flag && !flag3 && fadeOutSeconds <= 0.0) { return null; } int num6 = (flag ? 1 : channels); float[] array2 = new float[num * num6]; if (flag) { for (int i = 0; i < num; i++) { array2[i] = (array[i * 2] + array[i * 2 + 1]) * 0.5f; } } else { Array.Copy(array, 0, array2, 0, array2.Length); } if (flag3) { for (int j = 0; j < array2.Length; j++) { array2[j] = Mathf.Clamp(array2[j] * num4, -1f, 1f); } } int num7 = 0; if (fadeOutSeconds > 0.0) { num7 = (int)Math.Round(fadeOutSeconds * (double)custom.frequency); if (num7 > num) { num7 = num; } if (num7 > 0) { for (int k = 0; k < num7; k++) { float num8 = 1f - (float)(k + 1) / (float)num7; int num9 = (num - num7 + k) * num6; for (int l = 0; l < num6; l++) { array2[num9 + l] *= num8; } } } } AudioClip val = AudioClip.Create(((Object)custom).name + "_fit", num, num6, custom.frequency, false); if (!val.SetData(array2, 0)) { Object.Destroy((Object)(object)val); ModLog.Warn("подгонка '" + ((Object)original).name + "': SetData не сработал, оставляю оригинальный файл"); return null; } ModLog.Info(string.Format("подгонка '{0}': длина {1:0.00} c -> {2:0.00} c{3}, громкость пик {4:0.000} -> {5:0.000}{6}, каналы {7} -> {8}{9}", ((Object)original).name, custom.length, (float)num / (float)custom.frequency, flag2 ? " (обрезано)" : string.Empty, num3, flag3 ? (num3 * num4) : num3, flag3 ? " (нормализовано)" : string.Empty, channels, num6, (num7 > 0) ? (", затухание " + fadeOutSeconds.ToString("0.0") + " c") : string.Empty)); return val; } catch (Exception ex) { ModLog.Warn("подгонка '" + (((Object)(object)original != (Object)null) ? ((Object)original).name : "?") + "' не удалась: " + ex.Message); return null; } } private static float TryPeak(AudioClip clip) { try { int num = clip.samples * clip.channels; if (num <= 0) { return 0f; } float[] array = new float[num]; if (!clip.GetData(array, 0)) { return 0f; } return Peak(array, array.Length); } catch (Exception ex) { ModLog.Debug("пик оригинала '" + ((Object)clip).name + "' недоступен: " + ex.Message); return 0f; } } private static float Peak(float[] data, int count) { float num = 0f; for (int i = 0; i < count; i++) { float num2 = ((data[i] < 0f) ? (0f - data[i]) : data[i]); if (num2 > num) { num = num2; } } return num; } } public sealed class HarmonyAudioBackend : ISoundBackend { internal static class SourcePlayPatch { public static bool Prefix(AudioSource __instance) { //IL_004f: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)__instance == (Object)null) { return true; } AudioClip clip = __instance.clip; ObserveClip(clip); SoundReplacementManager manager = Manager; if (manager == null) { return true; } if (!manager.TryGet(clip, out var replacement)) { return true; } if ((Object)(object)replacement == (Object)null) { return false; } if (manager.ShouldSpatialize(((Object)clip).name)) { TrackHost.Play(((Component)__instance).transform.position, replacement, manager.Config, ((Object)clip).name); return false; } AudioPatchHelpers.AssignClip(__instance, replacement); return true; } } internal static class ClipArgumentPatch { public static bool Prefix(AudioSource __instance, ref AudioClip clip) { //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)clip == (Object)null) { return true; } ObserveClip(clip); SoundReplacementManager manager = Manager; if (manager == null) { return true; } if (!manager.TryGet(clip, out var replacement)) { return true; } if ((Object)(object)replacement == (Object)null) { return false; } if (manager.ShouldSpatialize(((Object)clip).name)) { TrackHost.Play(((Object)(object)__instance != (Object)null) ? ((Component)__instance).transform.position : Vector3.zero, replacement, manager.Config, ((Object)clip).name); return false; } clip = replacement; return true; } } internal static class SetClipPatch { public static void Postfix(AudioSource __instance, AudioClip value) { if (!((Object)(object)__instance == (Object)null) && !((Object)(object)value == (Object)null)) { SoundReplacementManager manager = Manager; if (manager != null && manager.PatchClipSetter && !manager.ShouldSpatialize(((Object)value).name) && manager.TryGetForAssignment(value, out var replacement) && !((Object)(object)replacement == (Object)null)) { AudioPatchHelpers.AssignClip(__instance, replacement); } } } } private const string HarmonyId = "com.soundreplacer.repo.audio"; private Harmony _harmony; private bool _installed; public string Name => "Harmony / AudioSource"; private static SoundReplacementManager Manager { get { if (!((Object)(object)Plugin.Instance == (Object)null)) { return Plugin.Instance.Replacements; } return null; } } public bool Install(SoundReplacementManager manager) { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Expected O, but got Unknown if (_installed) { return true; } if (_harmony == null) { _harmony = new Harmony("com.soundreplacer.repo.audio"); } int num = 0; int missing = 0; MethodInfo prefix = AccessTools.Method(typeof(SourcePlayPatch), "Prefix", (Type[])null, (Type[])null); MethodInfo prefix2 = AccessTools.Method(typeof(ClipArgumentPatch), "Prefix", (Type[])null, (Type[])null); MethodInfo postfix = AccessTools.Method(typeof(SetClipPatch), "Postfix", (Type[])null, (Type[])null); if (Patch(AccessTools.Method(typeof(AudioSource), "Play", new Type[0], (Type[])null), prefix, null, ref missing)) { num++; } if (Patch(AccessTools.Method(typeof(AudioSource), "Play", new Type[1] { typeof(ulong) }, (Type[])null), prefix, null, ref missing)) { num++; } if (Patch(AccessTools.Method(typeof(AudioSource), "PlayDelayed", new Type[1] { typeof(float) }, (Type[])null), prefix, null, ref missing)) { num++; } if (Patch(AccessTools.Method(typeof(AudioSource), "PlayScheduled", new Type[1] { typeof(double) }, (Type[])null), prefix, null, ref missing)) { num++; } if (Patch(AccessTools.Method(typeof(AudioSource), "PlayOneShot", new Type[1] { typeof(AudioClip) }, (Type[])null), prefix2, null, ref missing)) { num++; } if (Patch(AccessTools.Method(typeof(AudioSource), "PlayOneShot", new Type[2] { typeof(AudioClip), typeof(float) }, (Type[])null), prefix2, null, ref missing)) { num++; } if (Patch(AccessTools.Method(typeof(AudioSource), "PlayClipAtPoint", new Type[2] { typeof(Vector3), typeof(AudioClip) }, (Type[])null), prefix2, null, ref missing)) { num++; } if (Patch(AccessTools.Method(typeof(AudioSource), "PlayClipAtPoint", new Type[3] { typeof(Vector3), typeof(AudioClip), typeof(float) }, (Type[])null), prefix2, null, ref missing)) { num++; } if (Patch(AccessTools.Method(typeof(AudioSource), "set_clip", (Type[])null, (Type[])null), null, postfix, ref missing)) { num++; } _installed = num > 0; ModLog.Info("Harmony backend: установлено патчей " + num + ", недоступно " + missing); if (num == 0) { ModLog.Error("Harmony backend не установил ни одного патча — замена звуков работать не будет"); } return _installed; } public void Uninstall() { if (_harmony == null) { return; } try { _harmony.UnpatchSelf(); } catch (Exception ex) { ModLog.Warn("не удалось снять Harmony-патчи: " + ex.Message); } finally { _installed = false; } } private bool Patch(MethodInfo target, MethodInfo prefix, MethodInfo postfix, ref int missing) { //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Unknown result type (might be due to invalid IL or missing references) string text = ((target == null) ? "?" : (target.DeclaringType.Name + "." + target.Name)); if (target == null) { missing++; ModLog.Warn("метод для патча не найден: " + text); return false; } try { _harmony.Patch((MethodBase)target, (prefix == null) ? ((HarmonyMethod)null) : new HarmonyMethod(prefix), (postfix == null) ? ((HarmonyMethod)null) : new HarmonyMethod(postfix), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); ModLog.Debug("пропатчен " + text); return true; } catch (Exception ex) { ModLog.Warn("не удалось пропатчить " + text + ": " + ex.Message); return false; } } private static void ObserveClip(AudioClip clip) { if (!((Object)(object)clip == (Object)null) && !((Object)(object)Plugin.Instance == (Object)null)) { Plugin.Instance.Catalog?.Observe(clip); SoundReplacementManager replacements = Plugin.Instance.Replacements; if (replacements != null && replacements.LogPlays) { ModLog.Info("играет звук: '" + ((Object)clip).name + "' (" + clip.length.ToString("0.00") + " c, " + clip.channels + " кан.)"); } } } } internal static class AudioPatchHelpers { [ThreadStatic] private static bool _assigning; public static void AssignClip(AudioSource source, AudioClip clip) { if ((Object)(object)source == (Object)null || (Object)(object)clip == (Object)null || _assigning) { return; } _assigning = true; try { source.clip = clip; } catch (Exception ex) { ModLog.Warn("не удалось подменить клип у AudioSource: " + ex.Message); } finally { _assigning = false; } } } public interface ISoundBackend { string Name { get; } bool Install(SoundReplacementManager manager); void Uninstall(); } public sealed class SoundReplacementManager { private readonly ReplacerConfig _config; private readonly string _pluginDirectory; private readonly Dictionary<string, AudioClip> _replacements = new Dictionary<string, AudioClip>(StringComparer.Ordinal); private readonly Dictionary<string, string> _sources = new Dictionary<string, string>(StringComparer.Ordinal); private readonly List<string> _failures = new List<string>(); private readonly HashSet<string> _fitted = new HashSet<string>(StringComparer.Ordinal); private readonly List<AudioClip> _owned = new List<AudioClip>(); private readonly Dictionary<string, float> _lastStartBySource = new Dictionary<string, float>(StringComparer.OrdinalIgnoreCase); private readonly object _sync = new object(); public bool Enabled => _config.Enabled; public bool PatchClipSetter => _config.PatchClipSetter; public bool LogPlays => _config.LogPlays; public int Count { get { lock (_sync) { return _replacements.Count; } } } public IList<string> Failures { get { lock (_sync) { return _failures.ToArray(); } } } public ReplacerConfig Config => _config; public SoundReplacementManager(ReplacerConfig config, string pluginDirectory) { _config = config ?? new ReplacerConfig(); _pluginDirectory = pluginDirectory; } public bool IsNoTrim(string name) { if (string.IsNullOrEmpty(name)) { return false; } foreach (string item in _config.NoTrim) { if (string.Equals(item, name, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } public bool TryGet(AudioClip original, out AudioClip replacement) { replacement = null; if ((Object)(object)original == (Object)null || !_config.Enabled) { return false; } string name = ((Object)original).name; if (string.IsNullOrEmpty(name)) { return false; } lock (_sync) { if (!_replacements.TryGetValue(name, out var value) || (Object)(object)value == (Object)null) { return false; } if (value == original) { return false; } if (!_fitted.Contains(name)) { _fitted.Add(name); AudioClip val = ClipFitter.Fit(original, value, _config.MatchLength && !IsNoTrim(name), _config.MatchVolume, _config.MatchChannels, _config.FadeOutSeconds); if ((Object)(object)val != (Object)null) { _owned.Add(val); _replacements[name] = val; value = val; } } if (ShouldSuppress(name)) { replacement = null; return true; } replacement = value; return true; } } private bool ShouldSuppress(string name) { if (!_config.OncePerFile) { return false; } if (!_sources.TryGetValue(name, out var value) || string.IsNullOrEmpty(value)) { return false; } float unscaledTime = Time.unscaledTime; if (_lastStartBySource.TryGetValue(value, out var value2) && unscaledTime - value2 < (float)_config.OnceWindowSeconds) { ModLog.Info("повторный старт '" + name + "' заглушён (окно " + _config.OnceWindowSeconds.ToString("0.0") + " c)"); return true; } _lastStartBySource[value] = unscaledTime; return false; } public bool TryGetByName(string originalName, out AudioClip replacement) { replacement = null; if (string.IsNullOrEmpty(originalName) || !_config.Enabled) { return false; } lock (_sync) { if (!_replacements.TryGetValue(originalName, out var value) || (Object)(object)value == (Object)null) { return false; } if (ShouldSuppress(originalName)) { replacement = null; return true; } replacement = value; return true; } } public bool ShouldSpatialize(string name) { if (string.IsNullOrEmpty(name) || _config.Spatial.Count == 0) { return false; } for (int i = 0; i < _config.Spatial.Count; i++) { if (string.Equals(_config.Spatial[i], name, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } public bool TryGetForAssignment(AudioClip original, out AudioClip replacement) { replacement = null; if ((Object)(object)original == (Object)null || !_config.Enabled) { return false; } string name = ((Object)original).name; if (string.IsNullOrEmpty(name)) { return false; } lock (_sync) { if (!_replacements.TryGetValue(name, out var value) || (Object)(object)value == (Object)null) { return false; } if (value == original) { return false; } replacement = value; return true; } } public IEnumerator LoadAllCoroutine() { lock (_sync) { _replacements.Clear(); _sources.Clear(); _failures.Clear(); _fitted.Clear(); _owned.Clear(); _lastStartBySource.Clear(); } if (!_config.Enabled) { ModLog.Warn("замена звуков отключена в config.json (enabled = false)"); yield break; } int total = _config.Sounds.Count; if (total == 0) { ModLog.Info("config.json не содержит маппингов — звуки остаются оригинальными"); yield break; } ModLog.Info("загрузка пользовательских звуков, маппингов: " + total); int loaded = 0; foreach (KeyValuePair<string, string> sound in _config.Sounds) { string originalName = sound.Key; string configuredPath = sound.Value; if (!ReplacerConfig.TryResolvePath(_pluginDirectory, configuredPath, out var absolutePath, out var error)) { Fail(originalName, configuredPath, error); yield return null; continue; } string text = Path.GetExtension(absolutePath).ToLowerInvariant(); AudioClip val = null; string text2 = null; if (text == ".wav") { try { val = AudioFileLoader.LoadWav(absolutePath); } catch (Exception ex) { text2 = ex.Message; } } else if (text == ".ogg") { AudioClip decoded = null; string decodeError = null; yield return LoadOggCoroutine(absolutePath, delegate(AudioClip c) { decoded = c; }, delegate(string e) { decodeError = e; }); val = decoded; text2 = decodeError; } else { text2 = "неподдерживаемое расширение '" + text + "' (нужен .ogg или .wav)"; } if ((Object)(object)val == (Object)null) { Fail(originalName, configuredPath, text2 ?? "неизвестная ошибка декодирования"); yield return null; continue; } try { ((Object)val).name = "SR_" + SanitizeClipName(Path.GetFileNameWithoutExtension(absolutePath)); Register(originalName, val, configuredPath); loaded++; ModLog.Info("заменён звук '" + originalName + "' -> " + configuredPath); } catch (Exception ex2) { Fail(originalName, configuredPath, ex2.Message); } yield return null; absolutePath = null; } ModLog.Info("итог загрузки: заменено " + loaded + " из " + total + " звук(ов)"); } public void UnloadAll() { List<AudioClip> list; lock (_sync) { list = new List<AudioClip>(_owned); list.AddRange(_replacements.Values); _replacements.Clear(); _sources.Clear(); _owned.Clear(); _fitted.Clear(); _lastStartBySource.Clear(); } foreach (AudioClip item in list) { if (!((Object)(object)item == (Object)null)) { try { Object.Destroy((Object)(object)item); } catch (Exception ex) { ModLog.Warn("не удалось уничтожить AudioClip: " + ex.Message); } } } } private void Register(string originalName, AudioClip clip, string source) { lock (_sync) { if (_replacements.ContainsKey(originalName)) { ModLog.Warn("дубликат маппинга для '" + originalName + "', используется последний файл"); } _replacements[originalName] = clip; _sources[originalName] = source; _owned.Add(clip); } } private void Fail(string originalName, string path, string reason) { string text = "звук '" + originalName + "' не заменён: " + reason + " (файл: " + path + ") — играет оригинальный звук"; ModLog.Warn(text); lock (_sync) { _failures.Add(text); } } private static IEnumerator LoadOggCoroutine(string path, Action<AudioClip> onSuccess, Action<string> onError) { string absoluteUri; try { absoluteUri = new Uri(path).AbsoluteUri; } catch (Exception ex) { onError("не удалось построить file:// URI: " + ex.Message); yield break; } UnityWebRequest request = UnityWebRequestMultimedia.GetAudioClip(absoluteUri, (AudioType)14); try { yield return request.SendWebRequest(); if ((int)request.result != 1) { onError("Unity не смог декодировать OGG: " + (request.error ?? "unknown") + " (HTTP " + request.responseCode + ")"); yield break; } AudioClip content = DownloadHandlerAudioClip.GetContent(request); if ((Object)(object)content == (Object)null) { onError("декодер вернул null"); yield break; } if (content.samples <= 0) { onError("в декодированном клипе 0 сэмплов"); yield break; } onSuccess(content); } finally { ((IDisposable)request)?.Dispose(); } } private static string SanitizeClipName(string name) { if (string.IsNullOrEmpty(name)) { return "custom"; } StringBuilder stringBuilder = new StringBuilder(name.Length); foreach (char c in name) { if (char.IsLetterOrDigit(c) || c == '_' || c == '-' || c == ' ') { stringBuilder.Append(c); } else { stringBuilder.Append('_'); } if (stringBuilder.Length >= 80) { break; } } if (stringBuilder.Length != 0) { return stringBuilder.ToString(); } return "custom"; } } internal sealed class TrackHost : MonoBehaviour { internal static void Play(Vector3 position, AudioClip clip, ReplacerConfig config, string originalName) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Expected O, but got Unknown //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_015b: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)clip == (Object)null || config == null) { return; } try { GameObject val = new GameObject("SoundReplacerTrack"); val.transform.position = position; AudioSource val2 = val.AddComponent<AudioSource>(); val2.playOnAwake = false; val2.loop = false; val2.clip = clip; val2.volume = Mathf.Clamp01((float)config.TrackVolume); val2.spatialBlend = 1f; val2.rolloffMode = (AudioRolloffMode)0; val2.minDistance = Mathf.Max(0.1f, (float)config.MinDistance); val2.maxDistance = Mathf.Max(val2.minDistance + 0.1f, (float)config.MaxDistance); val2.dopplerLevel = 0f; val2.spread = 0f; val2.Play(); float num = 0.5f; if (config.Echo) { AudioEchoFilter val3 = val.AddComponent<AudioEchoFilter>(); val3.delay = Mathf.Clamp((float)config.EchoDelayMs, 10f, 5000f); val3.decayRatio = Mathf.Clamp((float)config.EchoDecayRatio, 0f, 0.95f); val3.wetMix = Mathf.Clamp01((float)config.EchoWetMix); val3.dryMix = Mathf.Clamp01((float)config.EchoDryMix); num += val3.delay * 0.001f * 6f; } Object.Destroy((Object)(object)val, clip.length + num); ModLog.Info(string.Format("трек для '{0}': 3D-источник в {1}, дистанции {2:0.0}-{3:0.0} м{4}", originalName, position, val2.minDistance, val2.maxDistance, config.Echo ? (", эхо " + config.EchoDelayMs.ToString("0") + " мс") : ", без эха")); } catch (Exception ex) { ModLog.Warn("не удалось запустить 3D-трек для '" + originalName + "': " + ex.Message); } } } }