diff --git a/DuckovMods.sln.DotSettings.user b/DuckovMods.sln.DotSettings.user index f745fa7..7861586 100644 --- a/DuckovMods.sln.DotSettings.user +++ b/DuckovMods.sln.DotSettings.user @@ -6,7 +6,10 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded + ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded @@ -17,14 +20,23 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded + ForceIncluded ForceIncluded + ForceIncluded + ForceIncluded ForceIncluded + ForceIncluded ForceIncluded + ForceIncluded ForceIncluded + ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded @@ -32,10 +44,17 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded diff --git a/HideCharacter/obj/Release/HideCharacter.AssemblyInfo.cs b/HideCharacter/obj/Release/HideCharacter.AssemblyInfo.cs index a890952..071b266 100644 --- a/HideCharacter/obj/Release/HideCharacter.AssemblyInfo.cs +++ b/HideCharacter/obj/Release/HideCharacter.AssemblyInfo.cs @@ -13,7 +13,7 @@ using System.Reflection; [assembly: System.Reflection.AssemblyCompanyAttribute("折纸的小箱子")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.1")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.1+0206a83f56b5a794fe2f173b4a047cc4f0d4cd90")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.1+5d69efbc3f80a5422cef0884e02fb27adf20b467")] [assembly: System.Reflection.AssemblyProductAttribute("HideCharacter")] [assembly: System.Reflection.AssemblyTitleAttribute("HideCharacter")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.1")] diff --git a/HideCharacter/obj/Release/HideCharacter.AssemblyInfoInputs.cache b/HideCharacter/obj/Release/HideCharacter.AssemblyInfoInputs.cache index 7243162..d3fc787 100644 --- a/HideCharacter/obj/Release/HideCharacter.AssemblyInfoInputs.cache +++ b/HideCharacter/obj/Release/HideCharacter.AssemblyInfoInputs.cache @@ -1 +1 @@ -0c41c29df034d7303af5a922ba9d31f37af5b0419be39221c78f9e624f5d890a +769c2595fb11f290049896c9b627f3b39daef1dc9f833f4947c915142e78d8d2 diff --git a/HideCharacter/obj/rider.project.restore.info b/HideCharacter/obj/rider.project.restore.info index 38a6735..ea4a121 100644 --- a/HideCharacter/obj/rider.project.restore.info +++ b/HideCharacter/obj/rider.project.restore.info @@ -1 +1 @@ -17619928534493660 \ No newline at end of file +17623343068138064 \ No newline at end of file diff --git a/HitFeedback/Api/ModConfigApi.cs b/HitFeedback/Api/ModConfigApi.cs new file mode 100644 index 0000000..8d617ea --- /dev/null +++ b/HitFeedback/Api/ModConfigApi.cs @@ -0,0 +1,488 @@ +using System; +using System.Linq; +using System.Reflection; +using UnityEngine; + +//替换为你的mod命名空间, 防止多个同名ModConfigAPI冲突 +namespace HitFeedback.Api { +/// +/// ModConfig 安全接口封装类 - 提供不抛异常的静态接口 +/// ModConfig Safe API Wrapper Class - Provides non-throwing static interfaces +/// +public static class ModConfigAPI +{ + public static string ModConfigName = "ModConfig"; + + //Ensure this match the number of ModConfig.ModBehaviour.VERSION + //这里确保版本号与ModConfig.ModBehaviour.VERSION匹配 + private const int ModConfigVersion = 1; + + private static string TAG = $"ModConfig_v{ModConfigVersion}"; + + private static Type modBehaviourType; + private static Type optionsManagerType; + public static bool isInitialized = false; + private static bool versionChecked = false; + private static bool isVersionCompatible = false; + + /// + /// 检查版本兼容性 + /// Check version compatibility + /// + private static bool CheckVersionCompatibility() + { + if (versionChecked) + return isVersionCompatible; + + try + { + // 尝试获取 ModConfig 的版本号 + // Try to get ModConfig version number + var versionField = modBehaviourType.GetField("VERSION", BindingFlags.Public | BindingFlags.Static); + if (versionField != null && versionField.FieldType == typeof(int)) + { + var modConfigVersion = (int)versionField.GetValue(null); + isVersionCompatible = (modConfigVersion == ModConfigVersion); + + if (!isVersionCompatible) + { + Debug.LogError($"[{TAG}] 版本不匹配!API版本: {ModConfigVersion}, ModConfig版本: {modConfigVersion}"); + return false; + } + + Debug.Log($"[{TAG}] 版本检查通过: {ModConfigVersion}"); + versionChecked = true; + return true; + } + else + { + // 如果找不到版本字段,发出警告但继续运行(向后兼容) + // If version field not found, warn but continue (backward compatibility) + Debug.LogWarning($"[{TAG}] 未找到版本信息字段,跳过版本检查"); + isVersionCompatible = true; + versionChecked = true; + return true; + } + } + catch (Exception ex) + { + Debug.LogError($"[{TAG}] 版本检查失败: {ex.Message}"); + isVersionCompatible = false; + versionChecked = true; + return false; + } + } + + /// + /// 初始化 ModConfigAPI,检查必要的函数是否存在 + /// Initialize ModConfigAPI, check if necessary functions exist + /// + public static bool Initialize() + { + try + { + if (isInitialized) + return true; + + // 获取 ModBehaviour 类型 + // Get ModBehaviour type + modBehaviourType = FindTypeInAssemblies("ModConfig.ModBehaviour"); + if (modBehaviourType == null) + { + Debug.LogWarning($"[{TAG}] ModConfig.ModBehaviour 类型未找到,ModConfig 可能未加载"); + return false; + } + + // 获取 OptionsManager_Mod 类型 + // Get OptionsManager_Mod type + optionsManagerType = FindTypeInAssemblies("ModConfig.OptionsManager_Mod"); + if (optionsManagerType == null) + { + Debug.LogWarning($"[{TAG}] ModConfig.OptionsManager_Mod 类型未找到"); + return false; + } + + // 检查版本兼容性 + // Check version compatibility + if (!CheckVersionCompatibility()) + { + Debug.LogWarning($"[{TAG}] ModConfig version mismatch!!!"); + return false; + } + + // 检查必要的静态方法是否存在 + // Check if necessary static methods exist + string[] requiredMethods = { + "AddDropdownList", + "AddInputWithSlider", + "AddBoolDropdownList", + "AddOnOptionsChangedDelegate", + "RemoveOnOptionsChangedDelegate", + }; + + foreach (var methodName in requiredMethods) + { + var method = modBehaviourType.GetMethod(methodName, BindingFlags.Public | BindingFlags.Static); + if (method == null) + { + Debug.LogError($"[{TAG}] 必要方法 {methodName} 未找到"); + return false; + } + } + + isInitialized = true; + Debug.Log($"[{TAG}] ModConfigAPI 初始化成功"); + return true; + } + catch (Exception ex) + { + Debug.LogError($"[{TAG}] 初始化失败: {ex.Message}"); + return false; + } + } + + /// + /// 在所有已加载的程序集中查找类型 + /// + private static Type FindTypeInAssemblies(string typeName) + { + try + { + // 获取当前域中的所有程序集 + Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); + + foreach (var assembly in assemblies) + { + try + { + // 检查程序集名称是否包含 ModConfig + if (assembly.FullName.Contains("ModConfig")) + { + Debug.Log($"[{TAG}] 找到 ModConfig 相关程序集: {assembly.FullName}"); + } + + // 尝试在该程序集中查找类型 + var type = assembly.GetType(typeName); + if (type != null) + { + Debug.Log($"[{TAG}] 在程序集 {assembly.FullName} 中找到类型 {typeName}"); + return type; + } + } + catch (Exception ex) + { + // 忽略单个程序集的查找错误 + continue; + } + } + + // 记录所有已加载的程序集用于调试 + Debug.LogWarning($"[{TAG}] 在所有程序集中未找到类型 {typeName},已加载程序集数量: {assemblies.Length}"); + foreach (var assembly in assemblies.Where(a => a.FullName.Contains("ModConfig"))) + { + Debug.Log($"[{TAG}] ModConfig 相关程序集: {assembly.FullName}"); + } + + return null; + } + catch (Exception ex) + { + Debug.LogError($"[{TAG}] 程序集扫描失败: {ex.Message}"); + return null; + } + } + + /// + /// 安全地添加选项变更事件委托 + /// Safely add options changed event delegate + /// + /// 事件处理委托,参数为变更的选项键名 + /// 是否成功添加 + public static bool SafeAddOnOptionsChangedDelegate(Action action) + { + if (!Initialize()) + return false; + + if (action == null) + { + Debug.LogWarning($"[{TAG}] 不能添加空的事件委托"); + return false; + } + + try + { + var method = modBehaviourType.GetMethod("AddOnOptionsChangedDelegate", BindingFlags.Public | BindingFlags.Static); + method.Invoke(null, new object[] { action }); + + Debug.Log($"[{TAG}] 成功添加选项变更事件委托"); + return true; + } + catch (Exception ex) + { + Debug.LogError($"[{TAG}] 添加选项变更事件委托失败: {ex.Message}"); + return false; + } + } + + /// + /// 安全地移除选项变更事件委托 + /// Safely remove options changed event delegate + /// + /// 要移除的事件处理委托 + /// 是否成功移除 + public static bool SafeRemoveOnOptionsChangedDelegate(Action action) + { + if (!Initialize()) + return false; + + if (action == null) + { + Debug.LogWarning($"[{TAG}] 不能移除空的事件委托"); + return false; + } + + try + { + var method = modBehaviourType.GetMethod("RemoveOnOptionsChangedDelegate", BindingFlags.Public | BindingFlags.Static); + method.Invoke(null, new object[] { action }); + + Debug.Log($"[{TAG}] 成功移除选项变更事件委托"); + return true; + } + catch (Exception ex) + { + Debug.LogError($"[{TAG}] 移除选项变更事件委托失败: {ex.Message}"); + return false; + } + } + + /// + /// 安全地添加下拉列表配置项 + /// Safely add dropdown list configuration item + /// + public static bool SafeAddDropdownList(string modName, string key, string description, System.Collections.Generic.SortedDictionary options, Type valueType, object defaultValue) + { + key = $"{modName}_{key}"; + + if (!Initialize()) + return false; + + try + { + var method = modBehaviourType.GetMethod("AddDropdownList", BindingFlags.Public | BindingFlags.Static); + method.Invoke(null, new object[] { modName, key, description, options, valueType, defaultValue }); + + Debug.Log($"[{TAG}] 成功添加下拉列表: {modName}.{key}"); + return true; + } + catch (Exception ex) + { + Debug.LogError($"[{TAG}] 添加下拉列表失败 {modName}.{key}: {ex.Message}"); + return false; + } + } + + /// + /// 安全地添加带滑条的输入框配置项 + /// Safely add input box with slider configuration item + /// + public static bool SafeAddInputWithSlider(string modName, string key, string description, Type valueType, object defaultValue, UnityEngine.Vector2? sliderRange = null) + { + key = $"{modName}_{key}"; + + if (!Initialize()) + return false; + + try + { + var method = modBehaviourType.GetMethod("AddInputWithSlider", BindingFlags.Public | BindingFlags.Static); + + // 处理可空参数 + // Handle nullable parameters + var parameters = sliderRange.HasValue ? + new object[] { modName, key, description, valueType, defaultValue, sliderRange.Value } : + new object[] { modName, key, description, valueType, defaultValue, null }; + + method.Invoke(null, parameters); + + Debug.Log($"[{TAG}] 成功添加滑条输入框: {modName}.{key}"); + return true; + } + catch (Exception ex) + { + Debug.LogError($"[{TAG}] 添加滑条输入框失败 {modName}.{key}: {ex.Message}"); + return false; + } + } + + /// + /// 安全地添加布尔下拉列表配置项 + /// Safely add boolean dropdown list configuration item + /// + public static bool SafeAddBoolDropdownList(string modName, string key, string description, bool defaultValue) + { + key = $"{modName}_{key}"; + + if (!Initialize()) + return false; + + try + { + var method = modBehaviourType.GetMethod("AddBoolDropdownList", BindingFlags.Public | BindingFlags.Static); + method.Invoke(null, new object[] { modName, key, description, defaultValue }); + + Debug.Log($"[{TAG}] 成功添加布尔下拉列表: {modName}.{key}"); + return true; + } + catch (Exception ex) + { + Debug.LogError($"[{TAG}] 添加布尔下拉列表失败 {modName}.{key}: {ex.Message}"); + return false; + } + } + + /// + /// 安全地加载配置值 + /// Safely load configuration value + /// + /// 值的类型 + /// 配置键 + /// 默认值 + /// 加载的值或默认值 + public static T SafeLoad(string mod_name, string key, T defaultValue = default(T)) + { + key = $"{mod_name}_{key}"; + + if (!Initialize()) + return defaultValue; + + if (string.IsNullOrEmpty(key)) + { + Debug.LogWarning($"[{TAG}] 配置键不能为空"); + return defaultValue; + } + + try + { + var loadMethod = optionsManagerType.GetMethod("Load", BindingFlags.Public | BindingFlags.Static); + if (loadMethod == null) + { + Debug.LogError($"[{TAG}] 未找到 OptionsManager_Mod.Load 方法"); + return defaultValue; + } + + // 获取泛型方法 + var genericLoadMethod = loadMethod.MakeGenericMethod(typeof(T)); + var result = genericLoadMethod.Invoke(null, new object[] { key, defaultValue }); + + Debug.Log($"[{TAG}] 成功加载配置: {key} = {result}"); + return (T)result; + } + catch (Exception ex) + { + Debug.LogError($"[{TAG}] 加载配置失败 {key}: {ex.Message}"); + return defaultValue; + } + } + + /// + /// 安全地保存配置值 + /// Safely save configuration value + /// + /// 值的类型 + /// 配置键 + /// 要保存的值 + /// 是否保存成功 + public static bool SafeSave(string mod_name, string key, T value) + { + key = $"{mod_name}_{key}"; + + if (!Initialize()) + return false; + + if (string.IsNullOrEmpty(key)) + { + Debug.LogWarning($"[{TAG}] 配置键不能为空"); + return false; + } + + try + { + var saveMethod = optionsManagerType.GetMethod("Save", BindingFlags.Public | BindingFlags.Static); + if (saveMethod == null) + { + Debug.LogError($"[{TAG}] 未找到 OptionsManager_Mod.Save 方法"); + return false; + } + + // 获取泛型方法 + var genericSaveMethod = saveMethod.MakeGenericMethod(typeof(T)); + genericSaveMethod.Invoke(null, new object[] { key, value }); + + Debug.Log($"[{TAG}] 成功保存配置: {key} = {value}"); + return true; + } + catch (Exception ex) + { + Debug.LogError($"[{TAG}] 保存配置失败 {key}: {ex.Message}"); + return false; + } + } + + /// + /// 检查 ModConfig 是否可用 + /// Check if ModConfig is available + /// + public static bool IsAvailable() + { + return Initialize(); + } + + /// + /// 获取 ModConfig 版本信息(如果存在) + /// Get ModConfig version information (if exists) + /// + public static string GetVersionInfo() + { + if (!Initialize()) + return "ModConfig 未加载 | ModConfig not loaded"; + + try + { + // 尝试获取版本信息(如果 ModBehaviour 有相关字段或属性) + // Try to get version information (if ModBehaviour has related fields or properties) + var versionField = modBehaviourType.GetField("VERSION", BindingFlags.Public | BindingFlags.Static); + if (versionField != null && versionField.FieldType == typeof(int)) + { + var modConfigVersion = (int)versionField.GetValue(null); + var compatibility = (modConfigVersion == ModConfigVersion) ? "兼容" : "不兼容"; + return $"ModConfig v{modConfigVersion} (API v{ModConfigVersion}, {compatibility})"; + } + + var versionProperty = modBehaviourType.GetProperty("VERSION", BindingFlags.Public | BindingFlags.Static); + if (versionProperty != null) + { + var versionValue = versionProperty.GetValue(null); + return versionValue?.ToString() ?? "未知版本 | Unknown version"; + } + + return "ModConfig 已加载(版本信息不可用) | ModConfig loaded (version info unavailable)"; + } + catch + { + return "ModConfig 已加载(版本检查失败) | ModConfig loaded (version check failed)"; + } + } + + /// + /// 检查版本兼容性 + /// Check version compatibility + /// + public static bool IsVersionCompatible() + { + if (!Initialize()) + return false; + return isVersionCompatible; + } +} +} \ No newline at end of file diff --git a/HitFeedback/Config.cs b/HitFeedback/Config.cs index 667c589..f24e4ae 100644 --- a/HitFeedback/Config.cs +++ b/HitFeedback/Config.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using UnityEngine; // 假设在Unity环境中使用Debug.LogError @@ -9,78 +11,140 @@ namespace HitFeedback public KeyCode hotKey = KeyCode.F8; public Dictionary probability = new Dictionary(); + /// + /// 当伤害具有这些特性之一时,将播放音频反馈。 + /// + public HashSet audioDamageFeatures = new HashSet(); + public void LoadConfig(string filename) { if (!File.Exists(filename)) { Debug.LogError($"Config file not found: {filename}"); - return; // 如果文件不存在,就没必要继续了 + return; } - // 清空旧的概率数据,确保每次加载都是从新开始 probability.Clear(); - + audioDamageFeatures.Clear(); // 清空旧的音频伤害特性数据 try { using (var sr = new StreamReader(filename)) { string line; - var lineNumber = 0; // 用于错误报告 + var lineNumber = 0; + string currentSection = ""; // 用于解析节 while ((line = sr.ReadLine()) != null) { lineNumber++; - line = line.Trim(); // 移除行首尾的空白字符 - + line = line.Trim(); // 忽略空行和注释行 if (string.IsNullOrEmpty(line) || line.StartsWith(";") || line.StartsWith("#")) { continue; } + // 处理节标题,例如 [General] 或 [AudioFeatures] + if (line.StartsWith("[") && line.EndsWith("]")) + { + currentSection = line.Substring(1, line.Length - 2).Trim(); + continue; // 跳过节标题行 + } + // 查找等号 var separatorIndex = line.IndexOf('='); if (separatorIndex == -1) { - Debug.LogWarning($"Skipping malformed line in config file '{filename}' at line {lineNumber}: No '=' found. Line: '{line}'"); + Debug.LogWarning( + $"Skipping malformed line in config file '{filename}' at line {lineNumber}: No '=' found. Line: '{line}'"); continue; } var key = line.Substring(0, separatorIndex).Trim(); var valueStr = line.Substring(separatorIndex + 1).Trim(); - - // 解析 hotKey - if (key.Equals("hotKey", System.StringComparison.OrdinalIgnoreCase)) + if (currentSection.Equals("General", StringComparison.OrdinalIgnoreCase)) { - try + // 解析 hotKey + if (key.Equals("hotKey", StringComparison.OrdinalIgnoreCase)) { - hotKey = (KeyCode)System.Enum.Parse(typeof(KeyCode), valueStr, true); - } - catch (System.ArgumentException) - { - Debug.LogError($"Invalid KeyCode '{valueStr}' in config file '{filename}' at line {lineNumber}. Using default F8."); - hotKey = KeyCode.F8; // 设置为默认值或保持不变 + try + { + hotKey = (KeyCode)Enum.Parse(typeof(KeyCode), valueStr, true); + } + catch (ArgumentException) + { + Debug.LogError( + $"Invalid KeyCode '{valueStr}' in config file '{filename}' at line {lineNumber}. Using default F8."); + hotKey = KeyCode.F8; + } } } - // 解析 probability 字典项 - else + else if (currentSection.Equals("Probabilities", StringComparison.OrdinalIgnoreCase)) { - if (float.TryParse(valueStr, out var probValue)) + // 解析 probability 字典项 + if (float.TryParse(valueStr, NumberStyles.Float, CultureInfo.InvariantCulture, + out var probValue)) { probability[key] = probValue; } else { - Debug.LogWarning($"Invalid float value '{valueStr}' for key '{key}' in config file '{filename}' at line {lineNumber}. Skipping entry."); + Debug.LogWarning( + $"Invalid float value '{valueStr}' for key '{key}' in config file '{filename}' at line {lineNumber}. Skipping entry."); } } + else if (currentSection.Equals("AudioFeatures", StringComparison.OrdinalIgnoreCase)) + { + // 解析 audioDamageFeatures 集合项 + // 键是 'feature' (或者可以随意定义,只要值是我们关心的) + // 值是 ExtendedDamageFeature 的枚举名称 + if (key.Equals("feature", StringComparison.OrdinalIgnoreCase)) // 假设所有特征都用同一个键"feature" + { + foreach (var featureName in valueStr.Split(new char[] { ',', '|' }, + StringSplitOptions.RemoveEmptyEntries)) + { + try + { + // 确保 ExtendedDamageFeature 是 [Flags] 枚举 + var feature = (DamageFeature)Enum.Parse(typeof(DamageFeature), + featureName.Trim(), true); + audioDamageFeatures.Add(feature); + } + catch (ArgumentException) + { + Debug.LogWarning( + $"Invalid ExtendedDamageFeature '{featureName.Trim()}' in config file '{filename}' at line {lineNumber}. Skipping entry."); + } + } + } + else + { + try + { + var feature = + (DamageFeature)Enum.Parse(typeof(DamageFeature), key, true); + if (bool.TryParse(valueStr, out bool includeFeature) && includeFeature) + { + audioDamageFeatures.Add(feature); + } + // 如果是 false,则不添加到集合,或者可以从集合中移除(如果默认是都包含) + } + catch (ArgumentException) + { + Debug.LogWarning( + $"Invalid ExtendedDamageFeature key '{key}' in config file '{filename}' at line {lineNumber}. Skipping entry."); + } + } + } + // 如果存在其他节,可以在这里添加 else if 处理 } } } - catch (System.Exception ex) + catch (Exception ex) { Debug.LogError($"Error reading config file '{filename}': {ex.Message}"); } } + /// /// 将当前配置存储到指定INI文件。 /// @@ -89,17 +153,17 @@ namespace HitFeedback { try { - // 确保目录存在 var directory = Path.GetDirectoryName(filename); if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { Directory.CreateDirectory(directory); } + using (var sw = new StreamWriter(filename)) { sw.WriteLine("; HitFeedback Configuration File"); sw.WriteLine("; Generated by HitFeedback.Config class"); - sw.WriteLine(); // 空行 + sw.WriteLine(); sw.WriteLine("[General]"); sw.WriteLine($"hotKey = {hotKey.ToString()}"); sw.WriteLine(); @@ -108,22 +172,113 @@ namespace HitFeedback sw.WriteLine("[Probabilities]"); foreach (var kvp in probability) { - // 使用 InvariantCulture 确保浮点数的格式在不同区域设置下都一致 - sw.WriteLine($"{kvp.Key} = {kvp.Value.ToString(System.Globalization.CultureInfo.InvariantCulture)}"); + sw.WriteLine($"{kvp.Key} = {kvp.Value.ToString(CultureInfo.InvariantCulture)}"); } } else { sw.WriteLine("; No probabilities currently configured."); } + + sw.WriteLine(); + if (audioDamageFeatures.Count > 0) + { + sw.WriteLine("[AudioFeatures]"); + // 假设每个特性一行,值为 true + foreach (var feature in audioDamageFeatures) + { + sw.WriteLine($"{feature.ToString()} = True"); + } + // 或者如果你想用一个键存储所有特性(用逗号或竖线分隔) + // sw.WriteLine($"features = {string.Join(",", audioDamageFeatures.Select(f => f.ToString()))}"); + } + else + { + sw.WriteLine("; No audio damage features currently configured."); + } + sw.WriteLine(); } + Debug.Log($"Config saved to: {filename}"); } - catch (System.Exception ex) + catch (Exception ex) { Debug.LogError($"Error saving config file '{filename}': {ex.Message}"); } } + /// + /// 根据DamageInfo的特性判断是否应该播放音频反馈。 + /// 如果DamageInfo的任何一个特性在audioDamageFeatures集合中,则返回true。 + /// + /// 要检查的DamageInfo对象。 + /// 如果应该播放音频反馈,则为true;否则为false。 + public bool ShouldPlayAudioFeedback(DamageInfo damageInfo) + { + if (audioDamageFeatures.Count == 0) + { + return false; + } + // 将DamageInfo的各种布尔属性/条件转换为DamageFeature组合 + DamageFeature currentDamageFeatures = DamageFeature.Undefined; + if (damageInfo.damageType == DamageTypes.normal) + { + currentDamageFeatures |= DamageFeature.NormalDamage; + } + else if (damageInfo.damageType == DamageTypes.realDamage) + { + currentDamageFeatures |= DamageFeature.RealDamage; + } + + if (damageInfo.isFromBuffOrEffect) + { + currentDamageFeatures |= DamageFeature.BuffOrEffectDamage; + } + if (damageInfo.ignoreArmor) + { + currentDamageFeatures |= DamageFeature.ArmorIgnoringDamage; + } + if (damageInfo.crit > 0) // crit > 0 表示是暴击 + { + currentDamageFeatures |= DamageFeature.CriticalDamage; + } + if (damageInfo.armorPiercing > 0) + { + currentDamageFeatures |= DamageFeature.ArmorPiercingDamage; + } + if (damageInfo.isExplosion) + { + currentDamageFeatures |= DamageFeature.ExplosionDamage; + } + if (damageInfo.armorBreak > 0) + { + currentDamageFeatures |= DamageFeature.ArmorBreakingDamage; + } + if (damageInfo.elementFactors != null && damageInfo.elementFactors.Count > 0) + { + currentDamageFeatures |= DamageFeature.ElementalDamage; + } + if (damageInfo.buffChance > 0 || damageInfo.buff != null) + { + currentDamageFeatures |= DamageFeature.OnHitBuffApply; + } + if (damageInfo.bleedChance > 0) + { + currentDamageFeatures |= DamageFeature.OnHitBleed; + } + + foreach (var configuredFeature in audioDamageFeatures) + { + if (configuredFeature == DamageFeature.Undefined) + { + continue; + } + if ((currentDamageFeatures & configuredFeature) == configuredFeature) + { + return true; + } + } + return false; + } } } diff --git a/HitFeedback/DamageFeature.cs b/HitFeedback/DamageFeature.cs new file mode 100644 index 0000000..c090450 --- /dev/null +++ b/HitFeedback/DamageFeature.cs @@ -0,0 +1,69 @@ +using System; + +namespace HitFeedback +{ + [Flags] + public enum DamageFeature + { + /// + /// 未指定或未分类的伤害特性。 + /// + Undefined = 0, + /// + /// 普通物理伤害或其他未特别定义的伤害(对应 DamageTypes.normal)。 + /// + NormalDamage = 1, + /// + /// 真实伤害,不受护甲或其他减伤效果影响(对应 DamageTypes.realDamage)。 + /// + RealDamage = 2, + /// + /// 伤害来自增益(Buff)或持续效果。 + /// (基于 DamageInfo.isFromBuffOrEffect) + /// + BuffOrEffectDamage = 4, + /// + /// 伤害无视目标的护甲。 + /// (基于 DamageInfo.ignoreArmor) + /// + ArmorIgnoringDamage = 8, + /// + /// 伤害是暴击伤害。 + /// (基于 DamageInfo.crit > 0) + /// + CriticalDamage = 16, + /// + /// 伤害包含护甲穿透效果。 + /// (基于 DamageInfo.armorPiercing > 0) + /// + ArmorPiercingDamage = 32, + /// + /// 伤害是爆炸类型。 + /// (基于 DamageInfo.isExplosion) + /// + ExplosionDamage = 64, + /// + /// 伤害具有护甲破坏效果。 + /// (基于 DamageInfo.armorBreak > 0) + /// + ArmorBreakingDamage = 128, + /// + /// 伤害可能带有元素效果(如果有 elementFactors 存在且非空)。 + /// + ElementalDamage = 256, + /// + /// 伤害可能附带Buff效果。 + /// (基于 DamageInfo.buffChance > 0 或 buff != null) + /// + /// + /// 注意:这个可以与 BuffOrEffectDamage 区分,BuffOrEffectDamage 是伤害本身来自Buff, + /// 而นี้是伤害造成时附带施加Buff的效果。 + /// + OnHitBuffApply = 512, + /// + /// 伤害可能附带流血效果。 + /// (基于 DamageInfo.bleedChance > 0) + /// + OnHitBleed = 1024, + } +} \ No newline at end of file diff --git a/HitFeedback/ModBehaviour.cs b/HitFeedback/ModBehaviour.cs index cf74c12..576378a 100644 --- a/HitFeedback/ModBehaviour.cs +++ b/HitFeedback/ModBehaviour.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using System.IO; using Duckov; +using HitFeedback.Api; using UnityEngine; using UnityEngine.Networking; using UnityEngine.SceneManagement; @@ -11,24 +12,26 @@ using Random = UnityEngine.Random; namespace HitFeedback { - public class ModBehaviour:Duckov.Modding.ModBehaviour + public class ModBehaviour : Duckov.Modding.ModBehaviour { public const string AudioFolderName = "audio"; public const string ConfigFileName = "config.ini"; public string audioFolderPath; public string configFilePath; - public Dictionary audioFilePath = new Dictionary(); - + public Dictionary audioProbability = new Dictionary(); + public Health health; - - public Config config=new Config(); - + + public Config config = new Config(); + public float totalWeight; + public const string MOD_SETTING_NAME = "受击反馈"; + private void Update() { - if (Input.GetKeyDown(KeyCode.F8)) + if (Input.GetKeyDown(config.hotKey)) { PlayRandomAudioClip(); } @@ -37,16 +40,16 @@ namespace HitFeedback protected override void OnAfterSetup() { LevelManager.OnLevelInitialized += OnSceneLoaded; - audioFolderPath=Path.Combine(info.path,AudioFolderName); - configFilePath=Path.Combine(info.path, ConfigFileName); + audioFolderPath = Path.Combine(info.path, AudioFolderName); + configFilePath = Path.Combine(info.path, ConfigFileName); FindWavFiles(); config.LoadConfig(configFilePath); ApplyConfig(); - foreach (var f in audioFilePath) - { - totalWeight+=f.Value; - } + + InitializeSetting(); + UpdateTotalWeight(); } + protected override void OnBeforeDeactivate() { LevelManager.OnLevelInitialized -= OnSceneLoaded; @@ -58,13 +61,22 @@ namespace HitFeedback SaveConfig(); } + private void UpdateTotalWeight() + { + totalWeight = 0; + foreach (var f in audioProbability) + { + totalWeight += f.Value; + } + } + private void ApplyConfig() { foreach (var f in config.probability) { - if (audioFilePath.ContainsKey(f.Key)) + if (audioProbability.ContainsKey(f.Key)) { - audioFilePath[f.Key] = f.Value; + audioProbability[f.Key] = f.Value; } } } @@ -72,28 +84,36 @@ namespace HitFeedback private void SaveConfig() { config.probability.Clear(); - foreach (var f in audioFilePath) + foreach (var f in audioProbability) { config.probability.Add(f.Key, f.Value); } config.SaveConfig(configFilePath); } + private void FindWavFiles() { - audioFilePath.Clear(); + audioProbability.Clear(); if (!Directory.Exists(audioFolderPath)) { return; } + try { - string[] files = Directory.GetFiles(audioFolderPath, "*.wav", SearchOption.TopDirectoryOnly); - if (files.Length > 0) + var audioFiles = new List(); + string[] wavFiles = Directory.GetFiles(audioFolderPath, "*.wav", SearchOption.TopDirectoryOnly); + audioFiles.AddRange(wavFiles); + string[] mp3Files = Directory.GetFiles(audioFolderPath, "*.mp3", SearchOption.TopDirectoryOnly); + audioFiles.AddRange(mp3Files); + string[] oggFiles = Directory.GetFiles(audioFolderPath, "*.ogg", SearchOption.TopDirectoryOnly); + audioFiles.AddRange(oggFiles); + if (audioFiles.Count > 0) { - foreach (string filePath in files) + foreach (var filePath in audioFiles) { - audioFilePath.Add(Path.GetFileName(filePath), 1); + audioProbability.Add(Path.GetFileName(filePath), 1); } } } @@ -120,8 +140,10 @@ namespace HitFeedback { if (health) { + health.OnHurtEvent.RemoveListener(OnHurtEvent); } + health = CharacterMainControl.Main?.Health; if (health) { @@ -132,15 +154,16 @@ namespace HitFeedback private void OnHurtEvent(DamageInfo damageInfo) { - PlayRandomAudioClip(); + if (config.ShouldPlayAudioFeedback(damageInfo)) + PlayRandomAudioClip(); } - + public void PlayRandomAudioClip() { - if (audioFilePath.Count > 0) + if (audioProbability.Count > 0) { var randomIndex = Random.Range(0, totalWeight); - foreach (var f in audioFilePath) + foreach (var f in audioProbability) { randomIndex -= f.Value; if (randomIndex <= 0) @@ -155,5 +178,182 @@ namespace HitFeedback Debug.LogWarning("Mod Feedback: No audio clips loaded to play."); } } + + public void InitializeSetting() + { + if (!Api.ModConfigAPI.Initialize()) + { + return; + } + + + foreach (var audio in audioProbability) + { + ModConfigAPI.SafeAddInputWithSlider(MOD_SETTING_NAME, audio.Key, $"音频\"{audio.Key}\"播放概率", + typeof(float), audio.Value, new Vector2(0, 100)); + } + + foreach (DamageFeature value in Enum.GetValues(typeof(DamageFeature))) + { + ModConfigAPI.SafeAddBoolDropdownList(MOD_SETTING_NAME, value.ToString(), + $"受到{ToSingleFeatureChineseString(value)}时触发", + config.audioDamageFeatures.Contains(value)); + } + + var hotkeyOptions = GenerateCommonKeyCodeOptions(); + ModConfigAPI.SafeAddDropdownList( + MOD_SETTING_NAME, + "hotKey", + "主动触发的热键", + hotkeyOptions, + typeof(int), + config.hotKey + ); + + + ModConfigAPI.SafeAddOnOptionsChangedDelegate(OnConfigChange); + } + + + + private void OnConfigChange(string key) + { + key = key[(MOD_SETTING_NAME.Length + 1)..]; + if (key == "hotKey") + { + config.hotKey = (KeyCode)ModConfigAPI.SafeLoad(MOD_SETTING_NAME, key, (int)(config.hotKey)); + return; + } + if (audioProbability.ContainsKey(key)) + { + var value = ModConfigAPI.SafeLoad(MOD_SETTING_NAME, key, audioProbability[key]); + audioProbability[key] = value; + } + + if (Enum.TryParse(key, out DamageFeature damageInfo)) + { + var current=config.audioDamageFeatures.Contains(damageInfo); + if (ModConfigAPI.SafeLoad(MOD_SETTING_NAME, key, current)) + { + config.audioDamageFeatures.Add(damageInfo); + } + else if (current) + { + config.audioDamageFeatures.Remove(damageInfo); + } + } + + UpdateTotalWeight(); + + } + + /// + /// 生成包含常用 KeyCode 的 SortedDictionary。 + /// + /// 一个 SortedDictionary,键是 KeyCode 的字符串表示,值是 KeyCode 枚举本身。 + private static SortedDictionary GenerateCommonKeyCodeOptions() + { + var options = new SortedDictionary(); + // 字母键 + for (var c = 'A'; c <= 'Z'; c++) + { + var keyCode = (int)(KeyCode)Enum.Parse(typeof(KeyCode), c.ToString()); + options.Add(c.ToString(), keyCode); + } + + // 数字键(主键盘) + for (var i = 0; i <= 9; i++) + { + var keyCode = (int)(KeyCode)Enum.Parse(typeof(KeyCode), "Alpha" + i.ToString()); + options.Add(i.ToString(), keyCode); + } + + // 数字键盘 + for (var i = 0; i <= 9; i++) + { + var keyCode = (int)(KeyCode)Enum.Parse(typeof(KeyCode), "Keypad" + i.ToString()); + options.Add($"Num_{i}", keyCode); // 加前缀区分主键盘数字 + } + + // 功能键 + for (var i = 1; i <= 12; i++) + { + var keyCode = (int)(KeyCode)Enum.Parse(typeof(KeyCode), "F" + i.ToString()); + options.Add($"F{i}", keyCode); + } + + // 常用控制键 + options.Add("空格", (int)KeyCode.Space); + options.Add("回车", (int)KeyCode.Return); + options.Add("Esc", (int)KeyCode.Escape); + options.Add("Shift (左)", (int)KeyCode.LeftShift); + options.Add("Shift (右)", (int)KeyCode.RightShift); + options.Add("Ctrl (左)", (int)KeyCode.LeftControl); + options.Add("Ctrl (右)", (int)KeyCode.RightControl); + options.Add("Alt (左)", (int)KeyCode.LeftAlt); + options.Add("Alt (右)", (int)KeyCode.RightAlt); + options.Add("Tab", (int)KeyCode.Tab); + options.Add("Backspace", (int)KeyCode.Backspace); + options.Add("Delete", (int)KeyCode.Delete); + options.Add("Home", (int)KeyCode.Home); + options.Add("End", (int)KeyCode.End); + options.Add("PageUp", (int)KeyCode.PageUp); + options.Add("PageDown", (int)KeyCode.PageDown); + options.Add("插入", (int)KeyCode.Insert); + // 方向键 + options.Add("向上", (int)KeyCode.UpArrow); + options.Add("向下", (int)KeyCode.DownArrow); + options.Add("向左", (int)KeyCode.LeftArrow); + options.Add("向右", (int)KeyCode.RightArrow); + // 鼠标按键 + options.Add("鼠标左键", (int)KeyCode.Mouse0); + options.Add("鼠标右键", (int)KeyCode.Mouse1); + options.Add("鼠标中键", (int)KeyCode.Mouse2); + // 其他一些常用键 + options.Add("~", (int)KeyCode.BackQuote); + options.Add("-", (int)KeyCode.Minus); + options.Add("=", (int)KeyCode.Equals); + options.Add("[", (int)KeyCode.LeftBracket); + options.Add("]", (int)KeyCode.RightBracket); + options.Add("\\", (int)KeyCode.Backslash); + options.Add(";", (int)KeyCode.Semicolon); + options.Add("'", (int)KeyCode.Quote); + options.Add(",", (int)KeyCode.Comma); + options.Add(".", (int)KeyCode.Period); + options.Add("/", (int)KeyCode.Slash); + return options; + } + private static string ToSingleFeatureChineseString(DamageFeature feature) + { + switch (feature) + { + case DamageFeature.Undefined: + return "未指定特性"; + case DamageFeature.NormalDamage: + return "普通伤害"; + case DamageFeature.RealDamage: + return "真实伤害"; + case DamageFeature.BuffOrEffectDamage: + return "增益/效果伤害"; + case DamageFeature.ArmorIgnoringDamage: + return "无视护甲伤害"; + case DamageFeature.CriticalDamage: + return "暴击伤害"; + case DamageFeature.ArmorPiercingDamage: + return "护甲穿透伤害"; + case DamageFeature.ExplosionDamage: + return "爆炸伤害"; + case DamageFeature.ArmorBreakingDamage: + return "护甲破坏伤害"; + case DamageFeature.ElementalDamage: + return "元素伤害"; + case DamageFeature.OnHitBuffApply: + return "命中附加增益"; + case DamageFeature.OnHitBleed: + return "命中附加流血"; + default: + return "未知特性"; // 处理未定义或将来添加的特性 + } + } } } \ No newline at end of file diff --git a/HitFeedback/obj/Release/HitFeedback.AssemblyInfo.cs b/HitFeedback/obj/Release/HitFeedback.AssemblyInfo.cs index 71318e8..e283802 100644 --- a/HitFeedback/obj/Release/HitFeedback.AssemblyInfo.cs +++ b/HitFeedback/obj/Release/HitFeedback.AssemblyInfo.cs @@ -13,7 +13,7 @@ using System.Reflection; [assembly: System.Reflection.AssemblyCompanyAttribute("HitFeedback")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+0206a83f56b5a794fe2f173b4a047cc4f0d4cd90")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+5d69efbc3f80a5422cef0884e02fb27adf20b467")] [assembly: System.Reflection.AssemblyProductAttribute("HitFeedback")] [assembly: System.Reflection.AssemblyTitleAttribute("HitFeedback")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/HitFeedback/obj/Release/HitFeedback.AssemblyInfoInputs.cache b/HitFeedback/obj/Release/HitFeedback.AssemblyInfoInputs.cache index 5943c9c..462cb5e 100644 --- a/HitFeedback/obj/Release/HitFeedback.AssemblyInfoInputs.cache +++ b/HitFeedback/obj/Release/HitFeedback.AssemblyInfoInputs.cache @@ -1 +1 @@ -044e9b3ac36cbf242c64f55f40a9077d40727cb97e540d335e1e29dbc7dccff1 +fa83406df4fd7108a6156ab0271b4cddce6abc73c66cbcefdb2f1419643481ef diff --git a/HitFeedback/obj/Release/HitFeedback.csproj.AssemblyReference.cache b/HitFeedback/obj/Release/HitFeedback.csproj.AssemblyReference.cache index 1b2ae5d..2f0b834 100644 Binary files a/HitFeedback/obj/Release/HitFeedback.csproj.AssemblyReference.cache and b/HitFeedback/obj/Release/HitFeedback.csproj.AssemblyReference.cache differ diff --git a/HitFeedback/obj/Release/HitFeedback.csproj.CoreCompileInputs.cache b/HitFeedback/obj/Release/HitFeedback.csproj.CoreCompileInputs.cache index b647b53..b7ef2d6 100644 --- a/HitFeedback/obj/Release/HitFeedback.csproj.CoreCompileInputs.cache +++ b/HitFeedback/obj/Release/HitFeedback.csproj.CoreCompileInputs.cache @@ -1 +1 @@ -17059da2b4ba38151f18bb4edeeed66662c124b29efddf4c48967bc01f547046 +86c7d211b447f2631e25f9656fa8ef4b9b63f5a671cedbbe00854ccaccdd00b3 diff --git a/HitFeedback/obj/Release/HitFeedback.dll b/HitFeedback/obj/Release/HitFeedback.dll index 35fcd84..a0ab00f 100644 Binary files a/HitFeedback/obj/Release/HitFeedback.dll and b/HitFeedback/obj/Release/HitFeedback.dll differ diff --git a/HitFeedback/obj/rider.project.restore.info b/HitFeedback/obj/rider.project.restore.info index b4b6f59..ea4a121 100644 --- a/HitFeedback/obj/rider.project.restore.info +++ b/HitFeedback/obj/rider.project.restore.info @@ -1 +1 @@ -17620100186005303 \ No newline at end of file +17623343068138064 \ No newline at end of file diff --git a/SceneSnapshot/PrintTool.cs b/SceneSnapshot/PrintTool.cs index 68fb704..3154fc9 100644 --- a/SceneSnapshot/PrintTool.cs +++ b/SceneSnapshot/PrintTool.cs @@ -1,490 +1,465 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Text; using UnityEngine; -using UnityEngine.EventSystems; using UnityEngine.SceneManagement; +using UnityEngine.EventSystems; // 用于UI射线检测 +using System; +using System.IO; +using System.Text; +using System.Reflection; +using System.Collections; // 用于 IEnumerable +using System.Collections.Generic; // 用于 HashSet, List, Dictionary +using System.Linq; // 用于 OrderBy namespace SceneSnapshot { internal class PrintTool : MonoBehaviour { - private const string FOLDER_NAME = "GameObjectSnapshots"; + private const string BASE_FOLDER_NAME = "GameObjectSnapshots"; // 主文件夹名称 private const int MAX_REFLECTION_DEPTH = 3; // 最大反射深度,防止循环引用或过深的对象图 private const int MAX_COLLECTION_ELEMENTS_TO_PRINT = 5; // 集合最多打印的元素数量 private void Update() { - if (Input.GetKeyDown(KeyCode.F2)) CaptureAndPrintSceneInfo(); + if (Input.GetKeyDown(KeyCode.F2)) + { + CaptureAndPrintSnapshot(); + } } - private void CaptureAndPrintSceneInfo() + private void CaptureAndPrintSnapshot() { - var desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); - var outputFolderPath = Path.Combine(desktopPath, FOLDER_NAME); + // 获取桌面路径 + string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); + // 1. 创建主文件夹 (如果不存在) + string baseOutputPath = Path.Combine(desktopPath, BASE_FOLDER_NAME); try { - if (!Directory.Exists(outputFolderPath)) + if (!Directory.Exists(baseOutputPath)) { - Directory.CreateDirectory(outputFolderPath); - Debug.Log($"创建输出文件夹: {outputFolderPath}"); + Directory.CreateDirectory(baseOutputPath); + Debug.Log($"已创建主快照文件夹: {baseOutputPath}"); } } - catch (Exception ex) + catch (Exception e) { - Debug.LogError($"创建文件夹失败: {outputFolderPath} - {ex.Message}"); + Debug.LogError($"无法创建主快照文件夹 {baseOutputPath}: {e.Message}"); return; } - var activeSceneName = SceneManager.GetActiveScene().name; - var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); - var fileName = $"{activeSceneName}_FullSnapshot_{timestamp}.txt"; // 修改文件名以示区别 - var fullFilePath = Path.Combine(outputFolderPath, fileName); - - var sb = new StringBuilder(); - - sb.AppendLine("================================================="); - sb.AppendLine($"场景信息快照 - 活跃场景: {activeSceneName}"); - sb.AppendLine($"生成时间: {DateTime.Now}"); - sb.AppendLine("================================================="); - sb.AppendLine(); - - sb.AppendLine("--- 鼠标位置对象信息 ---"); - AppendMouseHoverObjectInfo(sb); - sb.AppendLine(); - - sb.AppendLine("--- 所有加载场景的活跃游戏对象层次结构及其组件 ---"); - - // 遍历所有已加载的场景 - for (var i = 0; i < SceneManager.sceneCount; i++) - { - var currentScene = SceneManager.GetSceneAt(i); - - // 打印场景名称作为分割线 - sb.AppendLine($"\n===== 场景: {currentScene.name} ===== " + - (currentScene == SceneManager.GetActiveScene() ? "(活跃场景)" : "")); - - GameObject[] rootObjects = currentScene.GetRootGameObjects(); - if (rootObjects.Length == 0) - sb.AppendLine(" - 该场景没有根游戏对象。"); - else - foreach (var go in rootObjects) - AppendGameObjectInfo(go, 0, sb); - } - - sb.AppendLine("================================================="); - + // 2. 在主文件夹内创建带时间戳的子文件夹 + string timestampFolderName = DateTime.Now.ToString("yyyyMMdd_HHmmss"); + string currentSnapshotOutputPath = Path.Combine(baseOutputPath, timestampFolderName); try { - File.WriteAllText(fullFilePath, sb.ToString(), Encoding.UTF8); - Debug.Log($"场景信息已成功保存到: {fullFilePath}"); + if (!Directory.Exists(currentSnapshotOutputPath)) + { + Directory.CreateDirectory(currentSnapshotOutputPath); + } } - catch (Exception ex) + catch (Exception e) { - Debug.LogError($"保存文件失败: {fullFilePath} - {ex.Message}"); - } - } - - /// - /// 递归地将游戏对象的名称、活跃状态、组件及其子对象的层次结构追加到StringBuilder。 - /// **注意:此方法只会处理活跃状态为 activeSelf 的对象。** - /// - /// 要处理的游戏对象。 - /// 当前缩进级别。 - /// StringBuilder实例。 - private void AppendGameObjectInfo(GameObject go, int indentLevel, StringBuilder sb) - { - // 只有当对象自身是激活状态时才处理和打印 - if (!go || !go.activeSelf) return; - - var indent = new string(' ', indentLevel * 4); // 每个层级使用4个空格缩进 - - // 打印游戏对象名称和活跃状态 - sb.AppendLine( - $"{indent}[{go.name}] (ActiveSelf: {go.activeSelf}, ActiveInHierarchy: {go.activeInHierarchy})"); - - // 打印所有组件 - var components = go.GetComponents(); - foreach (var comp in components) - if (comp) // 某些组件可能在运行时被销毁 - sb.AppendLine($"{indent} - Component: {comp.GetType().Name}"); - - // 递归处理子对象 - foreach (Transform child in go.transform) AppendGameObjectInfo(child.gameObject, indentLevel + 1, sb); - } - - /// - /// 尝试检测鼠标位置下方的UI元素或场景对象,并将其路径和组件信息追加到StringBuilder。 - /// - /// StringBuilder实例。 - private void AppendMouseHoverObjectInfo(StringBuilder sb) - { - // 首先尝试Raycast UI元素 - var eventDataCurrentPosition = new PointerEventData(EventSystem.current); - eventDataCurrentPosition.position = new Vector2(Input.mousePosition.x, Input.mousePosition.y); - var results = new List(); - if (EventSystem.current) - { - EventSystem.current.RaycastAll(eventDataCurrentPosition, results); - } - - if (results.Count > 0) - { - // UI元素优先级更高 - var uiObject = results[0].gameObject; - var uiPath = GetGameObjectPath(uiObject); - sb.AppendLine($"鼠标下方UI路径: {uiPath}"); - sb.AppendLine($" - 所在场景: {uiObject.scene.name}"); - - // 添加UI对象组件信息 - sb.AppendLine(" - UI对象组件信息:"); - AppendGameObjectComponentInfo(sb, uiObject, " "); // 增加缩进 + Debug.LogError($"无法创建快照子文件夹 {currentSnapshotOutputPath}: {e.Message}"); return; } - // 如果没有UI元素,尝试Raycast场景对象 - if (Camera.main != null) - { - var ray = Camera.main.ScreenPointToRay(Input.mousePosition); - RaycastHit hit; - if (Physics.Raycast(ray, out hit)) - { - var sceneObjectPath = GetGameObjectPath(hit.collider.gameObject); - sb.AppendLine($"鼠标下方场景对象路径: {sceneObjectPath}"); - sb.AppendLine($" - 所在场景: {hit.collider.gameObject.scene.name}"); + Debug.Log($"开始生成场景快照到: {currentSnapshotOutputPath}"); - // 添加场景对象组件信息 - sb.AppendLine(" - 场景对象组件信息:"); - AppendGameObjectComponentInfo(sb, hit.collider.gameObject, " "); // 增加缩进 - return; + // Part 1: 打印所有对象的对象树及其组件 + PrintAllGameObjectsTree(currentSnapshotOutputPath); + + // Part 2: 打印鼠标位置对象的组件值 + PrintMouseHoveredObjectDetails(currentSnapshotOutputPath); + + Debug.Log("场景快照生成完毕!"); + } + + /// + /// 打印所有场景对象的对象树(包括DontDestroyOnLoad)及其组件。 + /// + private void PrintAllGameObjectsTree(string outputPath) + { + StringBuilder sb = new StringBuilder(); + sb.AppendLine("--- 所有激活场景对象树 ---"); + sb.AppendLine("--------------------------\n"); + + // 用于存储按场景分组的根对象 + var sceneRootGameObjects = new Dictionary>(); + // 用于存储 DontDestroyOnLoad 对象 + var dontDestroyOnLoadRoots = new List(); + + // 1. 遍历所有加载的场景,获取其根对象 + for (int i = 0; i < SceneManager.sceneCount; i++) + { + Scene scene = SceneManager.GetSceneAt(i); + sceneRootGameObjects[scene] = new List(scene.GetRootGameObjects()); + } + + // 2. 查找 DontDestroyOnLoad 对象 + // DontDestroyOnLoad 对象不属于任何通过 SceneManager.GetSceneAt 获取的“普通”场景 + // 它们通常在特殊的 "DontDestroyOnLoad" 场景中(在Unity编辑器中可见),但在运行时无法直接通过 SceneManager.GetSceneAt 访问。 + // 因此,我们遍历所有活跃的GameObject,找出那些是根对象但又不属于任何已知场景的。 + GameObject[] allActiveGameObjectsInHierarchy = FindObjectsOfType(); // 获取所有活跃的GameObject + + foreach (GameObject go in allActiveGameObjectsInHierarchy) + { + if (go.transform.parent == null) // 这是一个根对象 + { + bool foundInLoadedScene = false; + foreach (var kvp in sceneRootGameObjects) + { + if (kvp.Value.Contains(go)) + { + foundInLoadedScene = true; + break; + } + } + + if (!foundInLoadedScene) + { + // 如果它不是任何已加载场景的根对象,那么它可能是DontDestroyOnLoad对象 + dontDestroyOnLoadRoots.Add(go); + } + } + } + + // 3. 打印普通场景的对象树 + foreach (var kvp in sceneRootGameObjects) + { + Scene currentScene = kvp.Key; + List roots = kvp.Value; + + sb.AppendLine($"=== 场景: {currentScene.name} (路径: {currentScene.path}, 已加载: {currentScene.isLoaded}) ===\n"); + + // 按名称排序根对象以保持输出一致性 + foreach (GameObject root in roots.OrderBy(g => g.name)) + { + PrintGameObjectRecursive(root, 0, sb, new HashSet()); + } + } + + // 4. 打印 DontDestroyOnLoad 对象的对象树 + if (dontDestroyOnLoadRoots.Count > 0) + { + // 检查是否已经有一个伪的 "DontDestroyOnLoad" 场景被 Unity 在某些情境下自动添加 + // 如果是,为了避免重复,且让输出更清晰,可以先尝试移除这些。 + // 但是在 FindObjectsOfType 之后再分组,这种方式更健壮,不用管它是否有“场景” + + sb.AppendLine("\n=== DontDestroyOnLoad 对象 ===\n"); + foreach (GameObject root in dontDestroyOnLoadRoots.OrderBy(g => g.name)) + { + PrintGameObjectRecursive(root, 0, sb, new HashSet()); + } + } + + string filePath = Path.Combine(outputPath, "SceneObjectTree.txt"); + File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8); // 使用UTF8编码以支持更多字符 + Debug.Log($"场景对象树已保存到: {filePath}"); + } + + /// + /// 递归打印GameObject及其子级和组件。 + /// + private void PrintGameObjectRecursive(GameObject go, int depth, StringBuilder sb, HashSet visited) + { + // 防止循环引用或重复打印 + if (visited.Contains(go)) + { + sb.AppendLine($"{GetIndent(depth)}{go.name} (循环引用检测到!)"); + return; + } + visited.Add(go); + + string indent = GetIndent(depth); + sb.AppendLine($"{indent}GameObject: {go.name} (激活状态: {go.activeSelf}, 标签: {go.tag}, 层: {LayerMask.LayerToName(go.layer)})"); + + Component[] components = go.GetComponents(); + foreach (Component comp in components) + { + if (comp == null) continue; // 避免NRE,尽管不常见 + sb.AppendLine($"{indent} 组件: {comp.GetType().Name}"); + } + + for (int i = 0; i < go.transform.childCount; i++) + { + PrintGameObjectRecursive(go.transform.GetChild(i).gameObject, depth + 1, sb, visited); + } + + // 重要:在递归完成后通常不需要从visited中移除GameObject, + // 因为一个GameObject在对象树中只会以唯一的路径出现一次 + // (除非它以某种非常规方式被引用,但这不属于标准GameObject层级)。 + // 对于值类型或简单引用,可以在PrintObjectProperties中在处理完后移除。 + // 对于GameObject层级,一旦访问完其所有子节点,它在该“分支”的任务就完成了。 + // 如果不同根节点下可能会有相同的GameObject引用(例如通过Inspector引用), + // 那visited集合的作用是防止在*当前递归路径*中再次遇到同一个GameObject,从而避免死循环。 + // 对于整个场景树的打印,visited集合可以保持不变,因为我们不期望同一个GameObject作为不同根节点的子物体链中的一部分。 + // visited.Remove(go); // 对于GameObject树结构,这通常是不必要的,因为每个GameObject在树中只有一个父级。 + } + + /// + /// 打印鼠标位置对象的组件值。 + /// + private void PrintMouseHoveredObjectDetails(string outputPath) + { + StringBuilder sb = new StringBuilder(); + sb.AppendLine("--- 鼠标悬停对象详细信息 ---"); + sb.AppendLine("----------------------------\n"); + + GameObject hoveredObject = GetHoveredObject(); + + if (hoveredObject != null) + { + sb.AppendLine($"悬停的GameObject: {hoveredObject.name} (激活状态: {hoveredObject.activeSelf}, 标签: {hoveredObject.tag}, 层: {LayerMask.LayerToName(hoveredObject.layer)})"); + sb.AppendLine($"组件及其值:\n"); + + Component[] components = hoveredObject.GetComponents(); + foreach (Component comp in components) + { + if (comp == null) continue; + sb.AppendLine($" === 组件: {comp.GetType().Name} ==="); + // 使用反射打印组件的字段和属性值 + PrintObjectProperties(comp, 0, sb, new HashSet(), " "); // 初始缩进4个空格 + sb.AppendLine(); } } else { - sb.AppendLine("警告: 场景中没有主摄像机(Camera.main)或未被标记为 'MainCamera'。无法检测鼠标下的场景对象。"); + sb.AppendLine("当前鼠标下方没有对象。"); } - sb.AppendLine("鼠标位置处没有检测到UI元素或场景对象。"); + string filePath = Path.Combine(outputPath, "MouseHoverObjectDetails.txt"); + File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8); // 使用UTF8编码以支持更多字符 + Debug.Log($"鼠标悬停对象详情已保存到: {filePath}"); } /// - /// 为指定的GameObject追加其所有组件的信息。 + /// 获取鼠标下方的GameObject(优先UI,其次3D场景对象)。 /// - /// StringBuilder实例。 - /// 目标GameObject。 - /// 当前的缩进字符串。 - private void AppendGameObjectComponentInfo(StringBuilder sb, GameObject obj, string indent) + private GameObject GetHoveredObject() { - Component[] components = obj.GetComponents(); - if (components.Length == 0) + // 优先检测UI对象 + if (EventSystem.current != null) { - sb.AppendLine($"{indent} - 无组件"); + PointerEventData eventData = new PointerEventData(EventSystem.current); + eventData.position = Input.mousePosition; + List uiRaycastResults = new List(); + EventSystem.current.RaycastAll(eventData, uiRaycastResults); + + // 过滤掉非 interactable 的UI元素或者不包含 CanvasRenderer 的元素,可能更关注可见和可交互的UI + foreach (var result in uiRaycastResults) + { + if (result.gameObject != null && result.gameObject.GetComponent() != null) + { + return result.gameObject; // 返回第一个有效的UI元素 + } + } + } + else + { + Debug.LogWarning("场景中没有EventSystem,无法检测UI对象。请确保场景中存在一个EventSystem GameObject。"); + } + + // 如果没有UI对象,则检测3D场景对象 + if (Camera.main != null) + { + Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); + // 仅检测默认层,或可配置的层 + if (Physics.Raycast(ray, out RaycastHit hit)) + { + return hit.collider.gameObject; + } + } + else + { + Debug.LogWarning("场景中没有主摄像机(Tagged 'MainCamera'),无法进行3D射线检测。请确保主摄像机正确标记。"); + } + + return null; + } + + /// + /// 使用反射递归打印对象的字段和属性值。 + /// + /// 要打印的对象。 + /// 当前反射深度。 + /// StringBuilder用于构建输出。 + /// 用于检测循环引用的已访问对象集合。 + /// 当前缩进字符串。 + private void PrintObjectProperties(object obj, int currentDepth, StringBuilder sb, HashSet visitedObjects, string indent) + { + if (obj == null) + { + sb.AppendLine("null"); return; } - foreach (var component in components) + Type type = obj.GetType(); + + // 1. 处理基本类型、字符串、枚举 + // 注意:string是引用类型,但行为上通常被视为值类型,其ToString是其自身 + if (type.IsPrimitive || obj is string || type.IsEnum || obj is decimal || obj is DateTime) { - if (component == null) - { - sb.AppendLine($"{indent} - (空组件)"); - continue; - } - - // 尝试获取Behaviour的enabled状态 - string enabledStatus = (component is Behaviour behaviour) ? behaviour.enabled.ToString() : "N/A"; - sb.AppendLine($"{indent} - 组件: {component.GetType().Name} (Enabled: {enabledStatus})"); - AppendComponentPropertiesAndFields(sb, component, indent + " "); // 更深一层缩进,显示组件的属性/字段 - } - } - - /// - /// 利用反射为指定的组件追加其公共属性和字段的信息。 - /// - /// StringBuilder实例。 - /// 目标组件。 - /// 当前的缩进字符串。 - private void AppendComponentPropertiesAndFields(StringBuilder sb, Component component, string indent) - { - var bindingFlags = BindingFlags.Public | BindingFlags.Instance; - var componentType = component.GetType(); - // 收集所有公共实例属性和字段 - var members = new List(); - members.AddRange(componentType.GetProperties(bindingFlags)); - members.AddRange(componentType.GetFields(bindingFlags)); - bool hasPrintedAnything = false; - foreach (var member in members) - { - // 排除一些常见或特定组件上可能导致冗余或问题的属性/字段 - if (IsMemberToExclude(member, component)) continue; - object value = null; - string memberName = member.Name; - try - { - if (member is PropertyInfo pi) - { - if (pi.CanRead) // 确保属性可读 - { - value = pi.GetValue(component); - } - else - { - // 忽略不可读的属性 - continue; - } - } - else if (member is FieldInfo fi) - { - value = fi.GetValue(component); - } - } - catch (Exception ex) - { - // 捕获获取值时可能发生的异常 - sb.AppendLine($"{indent}- {memberName}: <获取失败: {ex.GetType().Name}>"); - hasPrintedAnything = true; - continue; - } - - // 格式化值进行显示,初始深度为0 - string formattedValue = FormatValueForDisplay(value, 0); - sb.AppendLine($"{indent}- {memberName}: {formattedValue}"); - hasPrintedAnything = true; + sb.AppendLine($"({type.Name}) {obj}"); // 显示类型名,更清晰 + return; } - if (!hasPrintedAnything) + // 2. 处理常见的Unity值类型(如Vector3, Quaternion, Color, Rect等) + // 这些类型通常有很好的ToString()方法,且不应过度深入反射其内部字段 + if (obj is Vector2 || obj is Vector3 || obj is Vector4 || obj is Quaternion || + obj is Color || obj is Color32 || obj is Rect || obj is Bounds || + obj is AnimationCurve || obj is LayerMask || obj is Matrix4x4) { - sb.AppendLine($"{indent}- (无公共属性或字段)"); + sb.AppendLine($"({type.Name}) {obj}"); // 显示类型名,更清晰 + return; } - } - - /// - /// 判断一个成员是否应该被排除在打印列表之外。 - /// 用于过滤掉冗余或可能导致深度递归的成员。 - /// - /// 要检查的MemberInfo。 - /// 该成员所属的组件实例。 - /// 如果应排除则返回true。 - private bool IsMemberToExclude(MemberInfo member, Component component) - { - // 排除从UnityEngine.Object继承的常见属性,这些通常是GameObject级别的元数据, - // 或者可能导致不必要的递归。 - // 特别是gameObject和transform,它们的类型就是GameObject和Transform,递归它们没有意义, - // 且其值就是组件所依附的GameObject和Transform,已经通过GetGameObjectPath显示了。 - switch (member.Name) + + // 3. 处理UnityEngine.Object类型(但不是Component或GameObject本身) + // 例如 Material, Texture, ScriptableObject等,通常只打印其名称或ToString()就足够 + if (typeof(UnityEngine.Object).IsAssignableFrom(type) && !(obj is Component) && !(obj is GameObject)) { - case "hideFlags": // Unity内部的标志,通常不需要显示 - case "name": // GameObject的名称,已在路径中显示 - case "tag": // GameObject的标签,可从GameObject直接获取 - case "layer": // GameObject的层,可从GameObject直接获取 - case "useGUILayout": // Unity内部GUI相关的,通常不作为组件值关心 - case "runInEditMode": // Unity编辑器模式相关,通常不作为组件值关心 - // 对于Component基类上的gameObject和transform属性,它们直接指向宿主对象和其Transform。 - // 打印它们本身就是重复的且可能误导(不是组件内部的独特“值”)。 - case "gameObject": - case "transform": - case "isStatic": // GameObject的isStatic状态 - return true; + // 对于这些Unity对象,ToString()通常会返回对象名和类型,足够了 + sb.AppendLine($"({type.Name}) {obj.ToString()}"); + return; } - - // 进一步过滤掉一些Unity内部或编辑器相关的属性,这些属性通常在运行时不提供有用的组件值信息。 - if (member.DeclaringType == typeof(Behaviour) || member.DeclaringType == typeof(MonoBehaviour)) - { - switch (member.Name) - { - case "isActiveAndEnabled": // 行为体的激活状态,通常与enabled一起考虑 - return true; - } - } - - return false; - } - - /// - /// 格式化对象值以进行显示,特别是针对Unity的常见类型,以避免循环打印和提供简洁输出。 - /// - /// 要格式化的值。 - /// 当前的递归深度。 - /// 格式化后的字符串。 - private string FormatValueForDisplay(object value, int currentDepth = 0) - { - if (value == null) return "null"; - Type type = value.GetType(); - // 深度限制检查:如果超过最大深度,则返回提示信息 + + // 4. 检查最大反射深度 if (currentDepth >= MAX_REFLECTION_DEPTH) { - // 对于集合,提供更具体一些的信息 - if (value is Array array1) return $"Array (Count: {array1.Length}, Max Depth Reached)"; - if (value is IList list1) return $"List (Count: {list1.Count}, Max Depth Reached)"; - if (value is IDictionary dictionary) return $"Dictionary (Count: {dictionary.Count}, Max Depth Reached)"; - return $"[{type.Name} (Max Depth Reached)]"; + sb.AppendLine($"{indent}... (达到最大反射深度)"); + return; } - // 1. 基本类型、字符串、枚举:直接ToString() - if (type.IsPrimitive || type == typeof(string) || type.IsEnum) + // 5. 检查循环引用(仅对引用类型有效,且不是字符串那种特殊引用类型) + if (!type.IsValueType && !type.IsPrimitive && !(obj is string)) { - return value.ToString(); + if (visitedObjects.Contains(obj)) + { + sb.AppendLine($"{indent}... (检测到循环引用: {type.Name})"); + return; + } + visitedObjects.Add(obj); // 标记为已访问 } - // 2. 常见的Unity结构体:特殊格式化,避免深度递归并提供简洁输出 - if (value is Vector2 vec2) return $"({vec2.x:F2}, {vec2.y:F2})"; - if (value is Vector3 vec3) return $"({vec3.x:F2}, {vec3.y:F2}, {vec3.z:F2})"; - if (value is Vector4 vec4) return $"({vec4.x:F2}, {vec4.y:F2}, {vec4.z:F2}, {vec4.w:F2})"; - - // Quaternion默认ToString()会显示x,y,z,w,但有时EulerAngles更直观。 - if (value is Quaternion q) - return - $"Q({q.x:F2}, {q.y:F2}, {q.z:F2}, {q.w:F2}) (Euler: {q.eulerAngles.x:F2}, {q.eulerAngles.y:F2}, {q.eulerAngles.z:F2})"; - - if (value is Color color) return $"RGBA({color.r:F2}, {color.g:F2}, {color.b:F2}, {color.a:F2})"; - if (value is Rect rect) - return $"Rect(Pos:({rect.xMin:F2},{rect.yMin:F2}) Size:({rect.width:F2},{rect.height:F2}))"; - if (value is Bounds bounds) - return - $"Bounds(Center:({bounds.center.x:F2},{bounds.center.y:F2},{bounds.center.z:F2}) Extents:({bounds.extents.x:F2},{bounds.extents.y:F2},{bounds.extents.z:F2}))"; - if (value is LayerMask layerMask) + // 6. 处理集合类型(数组、List、Dictionary等,但不包括字符串) + if (obj is IEnumerable enumerable) { - // LayerMask的值可能代表多个层,或一个单一层。LayerToName只能转换单一层。 - // 对于多个层,返回其原始值更有意义。 - return $"LayerMask(Value: {layerMask.value})"; + // 对于字典,直接打印IEnumerable会导致键值对混乱,需要特殊处理 + if (obj is IDictionary dictionary) + { + sb.AppendLine($"({type.Name}) Count={dictionary.Count} {{"); + int count = 0; + foreach (DictionaryEntry entry in dictionary) + { + if (count >= MAX_COLLECTION_ELEMENTS_TO_PRINT) + { + sb.AppendLine($"{indent + " "}...(已截断,显示了{MAX_COLLECTION_ELEMENTS_TO_PRINT}对键值)"); + break; + } + sb.Append($"{indent + " "}[Key]: "); + PrintObjectProperties(entry.Key, currentDepth + 1, sb, visitedObjects, indent + " "); // 额外的缩进 + sb.Append($"{indent + " "}[Value]: "); + PrintObjectProperties(entry.Value, currentDepth + 1, sb, visitedObjects, indent + " "); // 额外的缩进 + count++; + } + sb.AppendLine($"{indent}}}"); + } + else // 普通的IEnumerable(List, Array等) + { + int elementCount = 0; + if (obj is ICollection collection) + elementCount = collection.Count; + else if (obj is Array array) + elementCount = array.Length; + else // 对于无法直接获取Count的IEnumerable,需要遍历统计 + { + var list = new List(); + foreach (var item in enumerable) list.Add(item); + elementCount = list.Count; + enumerable = list; // 重新赋值为可以重复遍历的list + } + + sb.AppendLine($"({type.Name}) Count={elementCount} ["); + int count = 0; + foreach (var item in enumerable) + { + if (count >= MAX_COLLECTION_ELEMENTS_TO_PRINT) + { + sb.AppendLine($"{indent + " "}...(已截断,显示了{MAX_COLLECTION_ELEMENTS_TO_PRINT}个元素)"); + break; + } + sb.Append($"{indent + " "}- "); + PrintObjectProperties(item, currentDepth + 1, sb, visitedObjects, indent + " "); + count++; + } + sb.AppendLine($"{indent}]"); + } + + // 集合本身通常不直接导致循环引用其自身,其内部元素才可能。 + // 故在处理集合后可以从visitedObjects中移除集合对象,防止它阻止其他路径对它的访问。 + if (!type.IsValueType && !type.IsPrimitive && !(obj is string)) visitedObjects.Remove(obj); + return; } - // 3. GameObject/Component引用:只打印名称或类型,避免无限递归 - if (value is GameObject go) return $"GameObject:'{go.name}'"; - if (value is Component comp) - return $"Component:'{comp.GetType().Name}' on GameObject:'{comp.gameObject.name}'"; + // 7. 处理一般对象(类或结构体)的字段和属性 + sb.AppendLine($"({type.Name}) {{"); - // 4. 集合类型:现在会打印内容,调用专门的辅助方法 - if (value is Array array) + BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + + // 字段 + FieldInfo[] fields = type.GetFields(flags); + foreach (FieldInfo field in fields) { - return FormatCollectionForDisplay(array, currentDepth + 1); + if (field.IsStatic) continue; // 跳过静态字段 + if (field.IsDefined(typeof(ObsoleteAttribute), true)) continue; // 跳过过时字段 + + string propertyIndent = indent + " "; + sb.Append($"{propertyIndent}{field.Name}: "); + try + { + object fieldValue = field.GetValue(obj); + PrintObjectProperties(fieldValue, currentDepth + 1, sb, visitedObjects, propertyIndent); + } + catch (Exception e) + { + sb.AppendLine($"<无法获取值: {e.Message}>"); + } } - if (value is IList list) + // 属性 + PropertyInfo[] properties = type.GetProperties(flags); + foreach (PropertyInfo prop in properties) { - return FormatCollectionForDisplay(list, currentDepth + 1); + // 跳过特殊名称属性(如Unity内部的hideFlags)、不可读属性、带索引器属性、过时属性 + if (prop.IsSpecialName || !prop.CanRead || prop.GetIndexParameters().Length > 0 || + prop.IsDefined(typeof(ObsoleteAttribute), true)) continue; + + string propertyIndent = indent + " "; + sb.Append($"{propertyIndent}{prop.Name}: "); + try + { + object propValue = prop.GetValue(obj); + PrintObjectProperties(propValue, currentDepth + 1, sb, visitedObjects, propertyIndent); + } + catch (Exception e) + { + sb.AppendLine($"<无法获取值: {e.Message}>"); + } } - if (value is IDictionary dict) - { - return FormatCollectionForDisplay(dict, currentDepth + 1); - } + sb.AppendLine($"{indent}}}"); - // 对于其他不可转换为Array/IList/IDictionary的IEnumerable,但又不是字符串的类型 - if (value is IEnumerable enumerable && !(value is string)) - { - return FormatCollectionForDisplay(enumerable, currentDepth + 1); - } - - // 5. 其他复杂引用类型:只打印其类型名称,默认不深入其内部 (除非深度允许,但在深度限制前这里只会显示类型名) - return type.Name; // 例如: Material, Texture2D等,只显示类型名 + // 在对象处理完毕后,从已访问集合中移除(如果它是引用类型), + // 这允许在对象图的不同路径中再次遇到它(如果需要),但防止当前路径的循环。 + if (!type.IsValueType && !type.IsPrimitive && !(obj is string)) visitedObjects.Remove(obj); } /// - /// 格式化集合对象以进行显示,支持深度限制和元素数量限制。 + /// 获取指定深度的缩进字符串。 /// - /// 要格式化的集合。 - /// 当前的递归深度。 - /// 格式化后的集合字符串。 - private string FormatCollectionForDisplay(IEnumerable collection, int currentDepth) + private string GetIndent(int depth) { - if (collection == null) return "null collection"; - Type collectionType = collection.GetType(); - StringBuilder sb = new StringBuilder(); - // 尝试获取集合的类型名,去除可能的反引号(用于泛型类型) - string collectionTypeName = collectionType.IsGenericType - ? collectionType.Name.Substring(0, collectionType.Name.IndexOf('`')) - : collectionType.Name.Replace("[]", ""); - sb.Append(collectionTypeName); - - sb.Append(" ["); - int i = 0; - foreach (var item in collection) - { - if (i >= MAX_COLLECTION_ELEMENTS_TO_PRINT) - { - sb.Append(", ..."); - break; - } - - if (i > 0) sb.Append(", "); - if (item is DictionaryEntry entry) // 针对非泛型IDictionary - { - sb.Append( - $"{{{FormatValueForDisplay(entry.Key, currentDepth + 1)}: {FormatValueForDisplay(entry.Value, currentDepth + 1)}}}"); - } - else - { - Type itemType = item?.GetType(); - // 针对泛型IDictionary,其元素是KeyValuePair - if (itemType != null && itemType.IsGenericType && - itemType.GetGenericTypeDefinition() == typeof(KeyValuePair<,>)) - { - // 使用反射获取Key和Value属性 - PropertyInfo keyProperty = itemType.GetProperty("Key"); - PropertyInfo valueProperty = itemType.GetProperty("Value"); - if (keyProperty != null && valueProperty != null) - { - object key = keyProperty.GetValue(item); - object value = valueProperty.GetValue(item); - sb.Append( - $"{{{FormatValueForDisplay(key, currentDepth + 1)}: {FormatValueForDisplay(value, currentDepth + 1)}}}"); - } - else - { - // Fallback in case Key/Value properties aren't found - sb.Append(FormatValueForDisplay(item, currentDepth + 1)); - } - } - else - { - // 格式化普通集合元素 - sb.Append(FormatValueForDisplay(item, currentDepth + 1)); - } - } - - i++; - } - - // 尝试获取集合的实际数量 - string countInfo = "N/A"; - if (collection is ICollection c) - { - countInfo = c.Count.ToString(); - } - else if (collection is Array a) - { - countInfo = a.Length.ToString(); - } - - sb.Append($"] (Count: {countInfo})"); - return sb.ToString(); + return new string(' ', depth * 2); } - - /// - /// 获取给定游戏对象的完整层次路径。 - /// - /// 要获取路径的游戏对象。 - /// 游戏对象的完整路径,例如 "Parent/Child/Object"。 - private string GetGameObjectPath(GameObject go) - { - if (go == null) return "N/A"; - - var path = go.name; - var currentTransform = go.transform; - - while (currentTransform.parent != null) - { - currentTransform = currentTransform.parent; - path = currentTransform.name + "/" + path; - } - - return path; - } - } -} \ No newline at end of file +} + diff --git a/SceneSnapshot/obj/Release/SceneSnapshot.AssemblyInfo.cs b/SceneSnapshot/obj/Release/SceneSnapshot.AssemblyInfo.cs index 2f2c13f..0c828c0 100644 --- a/SceneSnapshot/obj/Release/SceneSnapshot.AssemblyInfo.cs +++ b/SceneSnapshot/obj/Release/SceneSnapshot.AssemblyInfo.cs @@ -13,7 +13,7 @@ using System.Reflection; [assembly: System.Reflection.AssemblyCompanyAttribute("折纸的小箱子")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+0206a83f56b5a794fe2f173b4a047cc4f0d4cd90")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+5d69efbc3f80a5422cef0884e02fb27adf20b467")] [assembly: System.Reflection.AssemblyProductAttribute("SceneSnapshot")] [assembly: System.Reflection.AssemblyTitleAttribute("SceneSnapshot")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0")] diff --git a/SceneSnapshot/obj/Release/SceneSnapshot.AssemblyInfoInputs.cache b/SceneSnapshot/obj/Release/SceneSnapshot.AssemblyInfoInputs.cache index 02ed8f4..527661e 100644 --- a/SceneSnapshot/obj/Release/SceneSnapshot.AssemblyInfoInputs.cache +++ b/SceneSnapshot/obj/Release/SceneSnapshot.AssemblyInfoInputs.cache @@ -1 +1 @@ -532b9b8318b6010ae03f608127d55a2c59d0b50d9243b633f698b5d460668837 +4cb78221af3251625ca99f740da024b47f366123d3acdfd330074e35f359044e diff --git a/SceneSnapshot/obj/Release/SceneSnapshot.dll b/SceneSnapshot/obj/Release/SceneSnapshot.dll index fa5efa2..956694f 100644 Binary files a/SceneSnapshot/obj/Release/SceneSnapshot.dll and b/SceneSnapshot/obj/Release/SceneSnapshot.dll differ diff --git a/SceneSnapshot/obj/rider.project.restore.info b/SceneSnapshot/obj/rider.project.restore.info index 31df8f7..ea4a121 100644 --- a/SceneSnapshot/obj/rider.project.restore.info +++ b/SceneSnapshot/obj/rider.project.restore.info @@ -1 +1 @@ -17619862901687226 \ No newline at end of file +17623343068138064 \ No newline at end of file diff --git a/Theme/ModBehaviour.cs b/Theme/ModBehaviour.cs index 9888b64..eb45b27 100644 --- a/Theme/ModBehaviour.cs +++ b/Theme/ModBehaviour.cs @@ -1,16 +1,18 @@ using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using UnityEngine; namespace Theme { - public class ModBehaviour:Duckov.Modding.ModBehaviour + public class ModBehaviour : Duckov.Modding.ModBehaviour { + private StringBuilder _outputStringBuilder; protected override void OnAfterSetup() { - base.OnAfterSetup(); - } - protected override void OnBeforeDeactivate() - { - base.OnBeforeDeactivate(); + } + } } \ No newline at end of file diff --git a/Theme/obj/Release/Theme.AssemblyInfo.cs b/Theme/obj/Release/Theme.AssemblyInfo.cs index 4920b0d..7d9da60 100644 --- a/Theme/obj/Release/Theme.AssemblyInfo.cs +++ b/Theme/obj/Release/Theme.AssemblyInfo.cs @@ -13,7 +13,7 @@ using System.Reflection; [assembly: System.Reflection.AssemblyCompanyAttribute("Theme")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+0206a83f56b5a794fe2f173b4a047cc4f0d4cd90")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+5d69efbc3f80a5422cef0884e02fb27adf20b467")] [assembly: System.Reflection.AssemblyProductAttribute("Theme")] [assembly: System.Reflection.AssemblyTitleAttribute("Theme")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/Theme/obj/Release/Theme.AssemblyInfoInputs.cache b/Theme/obj/Release/Theme.AssemblyInfoInputs.cache index ba29f83..146bfd7 100644 --- a/Theme/obj/Release/Theme.AssemblyInfoInputs.cache +++ b/Theme/obj/Release/Theme.AssemblyInfoInputs.cache @@ -1 +1 @@ -9389535ab653bc716c7ef81ccb8542bee3753b2c1f4be9c952401b4ec0aa9c5d +c1519aee9b121f1df8699539b785671792d60ca0acb5c9d1d51d6039c2fa5de5 diff --git a/Theme/obj/Release/Theme.csproj.CoreCompileInputs.cache b/Theme/obj/Release/Theme.csproj.CoreCompileInputs.cache new file mode 100644 index 0000000..9a839d7 --- /dev/null +++ b/Theme/obj/Release/Theme.csproj.CoreCompileInputs.cache @@ -0,0 +1 @@ +ff440d05ee1ce379e101695273a889a69b97880c4ff8ae9ffb67b8c73e02feef diff --git a/Theme/obj/Release/Theme.csproj.FileListAbsolute.txt b/Theme/obj/Release/Theme.csproj.FileListAbsolute.txt new file mode 100644 index 0000000..ef228a9 --- /dev/null +++ b/Theme/obj/Release/Theme.csproj.FileListAbsolute.txt @@ -0,0 +1,7 @@ +D:\steam\steamapps\common\Escape from Duckov\Duckov_Data\Mods\Theme\Theme.dll +D:\vs_project\DuckovMods\Theme\obj\Release\Theme.csproj.AssemblyReference.cache +D:\vs_project\DuckovMods\Theme\obj\Release\Theme.GeneratedMSBuildEditorConfig.editorconfig +D:\vs_project\DuckovMods\Theme\obj\Release\Theme.AssemblyInfoInputs.cache +D:\vs_project\DuckovMods\Theme\obj\Release\Theme.AssemblyInfo.cs +D:\vs_project\DuckovMods\Theme\obj\Release\Theme.csproj.CoreCompileInputs.cache +D:\vs_project\DuckovMods\Theme\obj\Release\Theme.dll diff --git a/Theme/obj/Release/Theme.dll b/Theme/obj/Release/Theme.dll new file mode 100644 index 0000000..a350c9e Binary files /dev/null and b/Theme/obj/Release/Theme.dll differ diff --git a/Theme/obj/rider.project.model.nuget.info b/Theme/obj/rider.project.model.nuget.info new file mode 100644 index 0000000..2931e15 --- /dev/null +++ b/Theme/obj/rider.project.model.nuget.info @@ -0,0 +1 @@ +17622424783469959 \ No newline at end of file diff --git a/Theme/obj/rider.project.restore.info b/Theme/obj/rider.project.restore.info index 2931e15..ea4a121 100644 --- a/Theme/obj/rider.project.restore.info +++ b/Theme/obj/rider.project.restore.info @@ -1 +1 @@ -17622424783469959 \ No newline at end of file +17623343068138064 \ No newline at end of file diff --git a/UIFrame/GameOriginMainMenuUI.cs b/UIFrame/GameOriginMainMenuUI.cs new file mode 100644 index 0000000..fbd8e01 --- /dev/null +++ b/UIFrame/GameOriginMainMenuUI.cs @@ -0,0 +1,73 @@ +using Duckov.UI; +using Duckov.Utilities; +using TMPro; +using UIFrame.Utilities; +using UnityEngine; +using UnityEngine.SceneManagement; +using UnityEngine.UI; + +namespace UIFrame +{ + public class GameOriginMainMenuUI + { + public GameObject mainMenuContainer; + public Image? title; + public TMP_Text[]? allTexts; + + public Sprite titleSprite; + + public bool linkMainMenu=false; + + public void Initialize() + { + SceneLoader.onAfterSceneInitialize += OnAfterSceneInitialize; + } + + public void Cleanup() + { + SceneLoader.onAfterSceneInitialize -= OnAfterSceneInitialize; + } + + private void OnAfterSceneInitialize(SceneLoadingContext sceneLoadingContext) + { + linkMainMenu = false; + LinkMainMenuObj(); + } + + public void LinkMainMenuObj() + { + mainMenuContainer = GameObject.Find("MainMenuContainer"); + if(!mainMenuContainer) + { + Debug.LogWarning("Could not find Main Menu Container"); + return; + } + Debug.Log("Main Menu Container initialized"); + allTexts = mainMenuContainer.GetComponentsInChildren(); + title = GameObjectTool.FindChildByName(mainMenuContainer.transform, "MainTitle")?.GetComponent(); + linkMainMenu = true; + + + } + + public bool SetFont(TMP_FontAsset font) + { + if(allTexts == null || allTexts.Length == 0) + return false; + foreach (var text in allTexts) + { + text.font = font; + } + return true; + } + + public bool SetTitle(Sprite texture) + { + titleSprite=texture; + if(title==null) + return false; + title.sprite = texture; + return true; + } + } +} \ No newline at end of file diff --git a/UIFrame/Manager/TextureManager.cs b/UIFrame/Manager/TextureManager.cs new file mode 100644 index 0000000..96f7a02 --- /dev/null +++ b/UIFrame/Manager/TextureManager.cs @@ -0,0 +1,7 @@ +namespace UIFrame.Manager +{ + public class TextureManager + { + + } +} \ No newline at end of file diff --git a/UIFrame/ModBehaviour.cs b/UIFrame/ModBehaviour.cs index 298d1f3..b32d992 100644 --- a/UIFrame/ModBehaviour.cs +++ b/UIFrame/ModBehaviour.cs @@ -1,19 +1,69 @@ +using System; +using Duckov.Modding; using Duckov.Options.UI; +using Duckov.UI; +using Duckov.UI.Animations; +using Duckov.Utilities; +using HarmonyLib; using UnityEngine; namespace UIFrame { public class ModBehaviour:Duckov.Modding.ModBehaviour { + private const string MOD_ID="UIFrame"; + + private GameObject? workerObject; + + private Harmony? harmony; + protected override void OnAfterSetup() { - Debug.Log("OnAfterSetup"); - + CreateAPIObject(); + if (harmony == null) + { + harmony=new HarmonyLib.Harmony(MOD_ID); + } + harmony.PatchAll(); + Test(); } + protected override void OnBeforeDeactivate() { - Debug.Log("OnBeforeDeactivate"); + ClearAPIObject(); + harmony?.UnpatchAll(MOD_ID); + harmony = null; + } + + private void CreateAPIObject() + { + if(workerObject) + return; + workerObject = new GameObject($"{MOD_ID}_APIObject"); + workerObject.AddComponent(); + } + + private void ClearAPIObject() + { + if(!workerObject) + return; + Destroy(workerObject); + workerObject = null; + } + + private void Test() + { + if(!UIFrameAPI.Initialize()) + return; + if (UIFrameAPI.SetGameTitle(@"C:\Users\Lenovo\Pictures\异噬.png")) + { + Debug.Log("设置标题完成"); + } + else + { + Debug.Log("标题设置失败"); + } } } } \ No newline at end of file diff --git a/UIFrame/Patch/PatchSceneLoaderLoadMainMenu.cs b/UIFrame/Patch/PatchSceneLoaderLoadMainMenu.cs new file mode 100644 index 0000000..6f9bf3f --- /dev/null +++ b/UIFrame/Patch/PatchSceneLoaderLoadMainMenu.cs @@ -0,0 +1,15 @@ + + +using UnityEngine; + +namespace UIFrame.Patch +{ + [HarmonyLib.HarmonyPatch(typeof(SceneLoader), "LoadMainMenu")] + public class PatchSceneLoaderLoadMainMenu + { + public static void Postfix() + { + Debug.Log("LoadMainMenu called"); + } + } +} \ No newline at end of file diff --git a/UIFrame/Singleton.cs b/UIFrame/Singleton.cs new file mode 100644 index 0000000..8947427 --- /dev/null +++ b/UIFrame/Singleton.cs @@ -0,0 +1,38 @@ +using System; + +namespace UIFrame +{ + public abstract class Singleton where T : class + { + private static T? _instance; + private static readonly object _lock = new object(); // 用于多线程安全 + public static T? Instance + { + get + { + // 在多线程环境下,确保只有一个线程能够创建实例 + lock (_lock) + { + if (_instance == null) + { + + _instance = Activator.CreateInstance(typeof(T), true) as T; + if (_instance == null) + { + throw new InvalidOperationException($"无法为类型 {typeof(T).Name} 创建单例实例。" + + "请确保它有一个私有的无参构造函数。"); + } + } + return _instance; + } + } + } + protected Singleton() + { + if (_instance != null) + { + throw new InvalidOperationException($"试图创建第二个单例实例 {typeof(T).Name}。请通过 Singleton<{typeof(T).Name}>.Instance 访问。"); + } + } + } +} \ No newline at end of file diff --git a/UIFrame/UIFrame.csproj b/UIFrame/UIFrame.csproj index 6c99785..27f765c 100644 --- a/UIFrame/UIFrame.csproj +++ b/UIFrame/UIFrame.csproj @@ -18,5 +18,8 @@ + + + diff --git a/UIFrame/UIFrameAPI.cs b/UIFrame/UIFrameAPI.cs index 7ca993d..3db42b7 100644 --- a/UIFrame/UIFrameAPI.cs +++ b/UIFrame/UIFrameAPI.cs @@ -1,7 +1,99 @@ +using System.Collections.Generic; +using UIFrameAPI; +using UnityEngine; + +//改为自己的命名空间更好,这是一个画布单元,一个命名空间一个画布 namespace UIFrame { - public class UIFrameAPI + //反射虽然很好用,但我认为用组件传递高效 + public static class UIFrameAPI { + private static UIFrameAPIComponent? _apiComponent; + private static bool createdCanvas = false; + private static readonly string NameSpace = typeof(UIFrameAPI).Namespace ?? "UIFrame"; + + public static Dictionary textureCache = new Dictionary(); + public static Dictionary spriteCache = new Dictionary(); + /// + /// 初始化API + /// + /// + public static bool Initialize() + { + if (_apiComponent!=null) + return true; + _apiComponent = Object.FindObjectOfType(); + return _apiComponent; + } + /// + /// 设置标题图片(游戏中的标题是图片) + /// + /// 图片路径 + /// + public static bool SetGameTitle(string imageFilePath) + { + var texture=LoadSprite(imageFilePath); + if(texture==null) + { + return false; + } + return _apiComponent&&_apiComponent.SetTitleImage(texture); + } + /// + /// 设置标题图片(游戏中的标题是图片) + /// + /// 贴图 + /// + public static bool SetGameTitle(Sprite sprite) + { + return _apiComponent&&_apiComponent.SetTitleImage(sprite); + } + + /// + /// 加载图片文件为Texture2D + /// Texture2D实际存储了图片,图片的数据会上传显卡 + /// 此函数默认加载后不保留内存备份,即不可读像素 + /// 函数会建立文件到图片的索引缓存 + /// + /// + /// + public static Texture2D? LoadImage(string imageFilePath) + { + if (!textureCache.ContainsKey(imageFilePath)) + { + var texture=_apiComponent.LoadTexture(imageFilePath); + if (texture != null) + { + textureCache[imageFilePath] = texture; + } + else + { + Debug.LogError($"加载图片:{imageFilePath}失败"); + return null; + } + } + return textureCache[imageFilePath]; + } + /// + /// 加载图片为Sprite + /// 直接简单的调用LoadImage再创建一个代表此图片的Sprite + /// Sprite只是表明了图片的处理方式,所以为了灵活性建议自己创建 + /// 此函数会缓存地址到Sprite的索引 + /// + /// + /// + public static Sprite? LoadSprite(string imageFilePath) + { + var texture=LoadImage(imageFilePath); + if (texture==null) + return null; + if (!spriteCache.ContainsKey(imageFilePath)) + { + spriteCache[imageFilePath] = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), Vector2.zero); + } + return spriteCache[imageFilePath]; + } + } } \ No newline at end of file diff --git a/UIFrame/UIFrameAPIComponent.cs b/UIFrame/UIFrameAPIComponent.cs new file mode 100644 index 0000000..0a0b5b7 --- /dev/null +++ b/UIFrame/UIFrameAPIComponent.cs @@ -0,0 +1,21 @@ +using TMPro; +using UnityEngine; + +namespace UIFrameAPI +{ + public abstract class UIFrameAPIComponent:MonoBehaviour + { + public abstract bool CreateCanvas(string name); + + //设置游戏主菜单的原版标题 + public abstract bool SetTitleImage(Sprite sprite); + + //创建一个TMP字体 + public abstract TMP_FontAsset CreateFontAsset(string fontFilePath); + + //设置游戏字体 + public abstract bool SetFont(TMP_FontAsset font); + + public abstract Texture2D? LoadTexture(string imageFilePath); + } +} \ No newline at end of file diff --git a/UIFrame/UIFrameWorker.cs b/UIFrame/UIFrameWorker.cs new file mode 100644 index 0000000..f9054ae --- /dev/null +++ b/UIFrame/UIFrameWorker.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using TMPro; +using UIFrameAPI; +using UnityEngine; +using UnityEngine.SceneManagement; +using UnityEngine.TextCore.LowLevel; + +namespace UIFrame +{ + public class UIFrameWorker : UIFrameAPIComponent + { + public GameOriginMainMenuUI gameOriginMainMenuUI = new GameOriginMainMenuUI(); + public Dictionary canvasDic = new Dictionary(); + + + private void Awake() + { + gameOriginMainMenuUI.Initialize(); + } + + private void OnDestroy() + { + gameOriginMainMenuUI.Cleanup(); + } + + + public override bool CreateCanvas(string name) + { + return false; + } + + public override bool SetTitleImage(Sprite sprite) + { + return gameOriginMainMenuUI.SetTitle(sprite); + } + + + public override TMP_FontAsset CreateFontAsset(string fontFilePath) + { + var font = Font.CreateDynamicFontFromOSFont(fontFilePath, 24); + var tmpFont = TMP_FontAsset.CreateFontAsset( + font, + samplingPointSize: 72, // 采样点大小,影响字体质量 + atlasPadding: 4, // 图集内字符间距 + renderMode: GlyphRenderMode.SDFAA, // 推荐使用 SDF 抗锯齿模式 + atlasWidth: 1024, // 图集宽度 (2的幂) + atlasHeight: 1024, // 图集高度 (2的幂) + atlasPopulationMode: AtlasPopulationMode.Dynamic, // 动态填充 + enableMultiAtlasSupport: true // 启用多图集支持 + ); + return tmpFont; + } + + public override bool SetFont(TMP_FontAsset font) + { + return gameOriginMainMenuUI.SetFont(font); + } + + public override Texture2D? LoadTexture(string imageFilePath) + { + return Utilities.ImageLoader.LoadImageFromFile(imageFilePath); + } + } +} \ No newline at end of file diff --git a/UIFrame/UIManager.cs b/UIFrame/UIManager.cs deleted file mode 100644 index 69328db..0000000 --- a/UIFrame/UIManager.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace UIFrame -{ - public static class UIManager - { - - } -} \ No newline at end of file diff --git a/UIFrame/Utilities/GameObjectTool.cs b/UIFrame/Utilities/GameObjectTool.cs new file mode 100644 index 0000000..33e28cc --- /dev/null +++ b/UIFrame/Utilities/GameObjectTool.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace UIFrame.Utilities +{ + public class GameObjectTool + { + /// + /// 在指定父对象下,查找第一个匹配名称的子GameObject(可以是孙子、曾孙等) + /// + /// 要查找的父Transform。 + /// 要查找的GameObject的名称。 + /// 找到的GameObject,如果未找到则返回null。 + public static GameObject? FindChildByName(Transform parent, string name) + { + // 查找父对象本身是否就是目标对象 + if (parent.name.Equals(name)) + { + return parent.gameObject; + } + // 遍历所有直接子对象 + foreach (Transform child in parent) + { + // 检查当前子对象是否是目标对象 + if (child.name.Equals(name)) + { + return child.gameObject; + } + // 递归查找子对象的子对象 + var found = FindChildByName(child, name); + if (found) + { + return found; + } + } + return null; + } + /// + /// 在指定父对象下,查找所有匹配名称的子GameObject(可以是孙子、曾孙等),不区分大小写。 + /// + /// 要查找的父Transform。 + /// 要查找的GameObject的名称。 + /// 所有找到的GameObject列表。 + public static List FindChildrenByName(Transform parent, string name) + { + var foundObjects = new List(); + FindChildrenByNameRecursive(parent, name, foundObjects); + return foundObjects; + } + private static void FindChildrenByNameRecursive(Transform currentTransform, string name, List foundObjects) + { + // 检查当前对象是否是目标对象 + if (currentTransform.name.Equals(name)) + { + foundObjects.Add(currentTransform.gameObject); + } + // 遍历所有子对象并递归查找 + foreach (Transform child in currentTransform) + { + FindChildrenByNameRecursive(child, name, foundObjects); + } + } + + + } +} \ No newline at end of file diff --git a/UIFrame/Utilities/ImageLoader.cs b/UIFrame/Utilities/ImageLoader.cs new file mode 100644 index 0000000..e051358 --- /dev/null +++ b/UIFrame/Utilities/ImageLoader.cs @@ -0,0 +1,67 @@ +using System; +using System.IO; +using UnityEngine; + +namespace UIFrame.Utilities +{ + public class ImageLoader + { + /// + /// 从指定文件路径加载图片并创建 Texture2D。 + /// 支持常用的图片格式 (如 .png, .jpg, .jpeg, .bmp, .tga) + /// + /// 图片文件的绝对路径。 + /// 如果为true,则创建一个新的Texture2D对象并加载图片数据。如果为false,它会尝试加载到默认的空白Texture2D对象上,但通常建议使用true。 + /// 指定纹理是否加载为线性颜色空间 (true) 或 sRGB 颜色空间 (false)。 + /// 加载成功的 Texture2D 对象,如果失败则返回 null。 + public static Texture2D? LoadImageFromFile(string filePath, bool createNewTexture = true, bool linear = false) + { + if (string.IsNullOrEmpty(filePath)) + { + Debug.LogError("ImageLoader: 图片文件路径为空或无效。"); + return null; + } + + if (!File.Exists(filePath)) + { + Debug.LogError($"ImageLoader: 文件不存在于路径: {filePath}"); + return null; + } + + Texture2D? texture = null; + try + { + var fileData = File.ReadAllBytes(filePath); + if (createNewTexture) + { + texture = new Texture2D(2, 2, TextureFormat.RGBA32, false, linear); + } + else if (texture == null) + { + Debug.LogError("ImageLoader: 未能提供现有Texture2D用于加载,且createNewTexture为false。"); + return null; + } + + var success = texture.LoadImage(fileData, true); + + if (!success) + { + Debug.LogError($"ImageLoader: 无法加载图片数据到Texture2D。请检查文件是否为有效的图片格式或是否损坏: {filePath}"); + UnityEngine.Object.Destroy(texture); // 销毁失败的纹理对象 + return null; + } + return texture; + } + catch (Exception ex) + { + Debug.LogError($"ImageLoader: 加载图片时发生错误: {filePath} - {ex.Message}"); + if (texture != null) + { + UnityEngine.Object.Destroy(texture); // 发生异常时销毁已创建的纹理 + } + + return null; + } + } + } +} \ No newline at end of file diff --git a/UIFrame/obj/Release/UIFrame.AssemblyInfo.cs b/UIFrame/obj/Release/UIFrame.AssemblyInfo.cs index 1a1154a..d064d97 100644 --- a/UIFrame/obj/Release/UIFrame.AssemblyInfo.cs +++ b/UIFrame/obj/Release/UIFrame.AssemblyInfo.cs @@ -13,7 +13,7 @@ using System.Reflection; [assembly: System.Reflection.AssemblyCompanyAttribute("UIFrame")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+0206a83f56b5a794fe2f173b4a047cc4f0d4cd90")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+5d69efbc3f80a5422cef0884e02fb27adf20b467")] [assembly: System.Reflection.AssemblyProductAttribute("UIFrame")] [assembly: System.Reflection.AssemblyTitleAttribute("UIFrame")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/UIFrame/obj/Release/UIFrame.AssemblyInfoInputs.cache b/UIFrame/obj/Release/UIFrame.AssemblyInfoInputs.cache index b32f3f6..2106e86 100644 --- a/UIFrame/obj/Release/UIFrame.AssemblyInfoInputs.cache +++ b/UIFrame/obj/Release/UIFrame.AssemblyInfoInputs.cache @@ -1 +1 @@ -eb1c4a5bd10a6004eb2efc2bf4354383e0389ddf5e8c9585686a601d75e8a73a +2fde23c3fee254f6fc8f4689686f8631f1cec9f5b900327d5b657c6830a03267 diff --git a/UIFrame/obj/Release/UIFrame.assets.cache b/UIFrame/obj/Release/UIFrame.assets.cache index 3bc61fd..48390a8 100644 Binary files a/UIFrame/obj/Release/UIFrame.assets.cache and b/UIFrame/obj/Release/UIFrame.assets.cache differ diff --git a/UIFrame/obj/Release/UIFrame.csproj.AssemblyReference.cache b/UIFrame/obj/Release/UIFrame.csproj.AssemblyReference.cache index 2f0b834..df21979 100644 Binary files a/UIFrame/obj/Release/UIFrame.csproj.AssemblyReference.cache and b/UIFrame/obj/Release/UIFrame.csproj.AssemblyReference.cache differ diff --git a/UIFrame/obj/Release/UIFrame.csproj.CoreCompileInputs.cache b/UIFrame/obj/Release/UIFrame.csproj.CoreCompileInputs.cache index c51f270..9455748 100644 --- a/UIFrame/obj/Release/UIFrame.csproj.CoreCompileInputs.cache +++ b/UIFrame/obj/Release/UIFrame.csproj.CoreCompileInputs.cache @@ -1 +1 @@ -26cd918e8bf85dee638761fbef4e2117b1734f804498932e0dc10264a1740877 +8df4ddcbe4b7016ad51bf9e0b9ea8b606b36f4d0bbae7f1650b893a49036873e diff --git a/UIFrame/obj/Release/UIFrame.dll b/UIFrame/obj/Release/UIFrame.dll index 066342a..d78d46f 100644 Binary files a/UIFrame/obj/Release/UIFrame.dll and b/UIFrame/obj/Release/UIFrame.dll differ diff --git a/UIFrame/obj/UIFrame.csproj.nuget.dgspec.json b/UIFrame/obj/UIFrame.csproj.nuget.dgspec.json index 086340a..bb8c193 100644 --- a/UIFrame/obj/UIFrame.csproj.nuget.dgspec.json +++ b/UIFrame/obj/UIFrame.csproj.nuget.dgspec.json @@ -49,6 +49,12 @@ "frameworks": { "netstandard2.1": { "targetAlias": "netstandard2.1", + "dependencies": { + "Lib.Harmony": { + "target": "Package", + "version": "[2.4.1, )" + } + }, "imports": [ "net461", "net462", diff --git a/UIFrame/obj/project.assets.json b/UIFrame/obj/project.assets.json index 7f9867a..1f9dacb 100644 --- a/UIFrame/obj/project.assets.json +++ b/UIFrame/obj/project.assets.json @@ -1,11 +1,165 @@ { "version": 3, "targets": { - ".NETStandard,Version=v2.1": {} + ".NETStandard,Version=v2.1": { + "Lib.Harmony/2.4.1": { + "type": "package", + "dependencies": { + "Lib.Harmony.Ref": "2.4.1" + }, + "compile": { + "lib/netstandard2.0/_._": {} + }, + "runtime": { + "lib/netstandard2.0/_._": {} + } + }, + "Lib.Harmony.Ref/2.4.1": { + "type": "package", + "dependencies": { + "System.Reflection.Emit": "4.7.0" + }, + "compile": { + "ref/netstandard2.0/0Harmony.dll": { + "related": ".xml" + } + } + }, + "System.Reflection.Emit/4.7.0": { + "type": "package", + "compile": { + "ref/netstandard2.1/_._": {} + }, + "runtime": { + "lib/netstandard2.1/_._": {} + } + } + } + }, + "libraries": { + "Lib.Harmony/2.4.1": { + "sha512": "iLTZi/kKKB18jYEIwReZSx2xXyVUh4J1swReMgvYBBBn4tzA1Nd0PJlVyntY5BDdSiXSxzmvjc/3OYfFs0YwFg==", + "type": "package", + "path": "lib.harmony/2.4.1", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "HarmonyLogo.png", + "LICENSE", + "README.md", + "lib.harmony.2.4.1.nupkg.sha512", + "lib.harmony.nuspec", + "lib/net35/0Harmony.dll", + "lib/net35/0Harmony.pdb", + "lib/net35/0Harmony.xml", + "lib/net452/0Harmony.dll", + "lib/net452/0Harmony.pdb", + "lib/net452/0Harmony.xml", + "lib/net472/0Harmony.dll", + "lib/net472/0Harmony.pdb", + "lib/net472/0Harmony.xml", + "lib/net48/0Harmony.dll", + "lib/net48/0Harmony.pdb", + "lib/net48/0Harmony.xml", + "lib/net5.0/0Harmony.dll", + "lib/net5.0/0Harmony.pdb", + "lib/net5.0/0Harmony.xml", + "lib/net6.0/0Harmony.dll", + "lib/net6.0/0Harmony.pdb", + "lib/net6.0/0Harmony.xml", + "lib/net7.0/0Harmony.dll", + "lib/net7.0/0Harmony.pdb", + "lib/net7.0/0Harmony.xml", + "lib/net8.0/0Harmony.dll", + "lib/net8.0/0Harmony.pdb", + "lib/net8.0/0Harmony.xml", + "lib/net9.0/0Harmony.dll", + "lib/net9.0/0Harmony.pdb", + "lib/net9.0/0Harmony.xml", + "lib/netcoreapp3.0/0Harmony.dll", + "lib/netcoreapp3.0/0Harmony.pdb", + "lib/netcoreapp3.0/0Harmony.xml", + "lib/netcoreapp3.1/0Harmony.dll", + "lib/netcoreapp3.1/0Harmony.pdb", + "lib/netcoreapp3.1/0Harmony.xml", + "lib/netstandard2.0/_._" + ] + }, + "Lib.Harmony.Ref/2.4.1": { + "sha512": "+u1y2Qd6OlSUQ8JtrsrSo3adnAsrXMJ2YPYtbW+FAmdPI5yw34M9VX4bKl8ZwRuUzaGzZIz+oGMbn/yS4fWtZw==", + "type": "package", + "path": "lib.harmony.ref/2.4.1", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "HarmonyLogo.png", + "LICENSE", + "README.md", + "lib.harmony.ref.2.4.1.nupkg.sha512", + "lib.harmony.ref.nuspec", + "ref/netstandard2.0/0Harmony.dll", + "ref/netstandard2.0/0Harmony.xml" + ] + }, + "System.Reflection.Emit/4.7.0": { + "sha512": "VR4kk8XLKebQ4MZuKuIni/7oh+QGFmZW3qORd1GvBq/8026OpW501SzT/oypwiQl4TvT8ErnReh/NzY9u+C6wQ==", + "type": "package", + "path": "system.reflection.emit/4.7.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "lib/MonoAndroid10/_._", + "lib/MonoTouch10/_._", + "lib/net45/_._", + "lib/netcore50/System.Reflection.Emit.dll", + "lib/netcoreapp2.0/_._", + "lib/netstandard1.1/System.Reflection.Emit.dll", + "lib/netstandard1.1/System.Reflection.Emit.xml", + "lib/netstandard1.3/System.Reflection.Emit.dll", + "lib/netstandard2.0/System.Reflection.Emit.dll", + "lib/netstandard2.0/System.Reflection.Emit.xml", + "lib/netstandard2.1/_._", + "lib/xamarinios10/_._", + "lib/xamarinmac20/_._", + "lib/xamarintvos10/_._", + "lib/xamarinwatchos10/_._", + "ref/MonoAndroid10/_._", + "ref/MonoTouch10/_._", + "ref/net45/_._", + "ref/netcoreapp2.0/_._", + "ref/netstandard1.1/System.Reflection.Emit.dll", + "ref/netstandard1.1/System.Reflection.Emit.xml", + "ref/netstandard1.1/de/System.Reflection.Emit.xml", + "ref/netstandard1.1/es/System.Reflection.Emit.xml", + "ref/netstandard1.1/fr/System.Reflection.Emit.xml", + "ref/netstandard1.1/it/System.Reflection.Emit.xml", + "ref/netstandard1.1/ja/System.Reflection.Emit.xml", + "ref/netstandard1.1/ko/System.Reflection.Emit.xml", + "ref/netstandard1.1/ru/System.Reflection.Emit.xml", + "ref/netstandard1.1/zh-hans/System.Reflection.Emit.xml", + "ref/netstandard1.1/zh-hant/System.Reflection.Emit.xml", + "ref/netstandard2.0/System.Reflection.Emit.dll", + "ref/netstandard2.0/System.Reflection.Emit.xml", + "ref/netstandard2.1/_._", + "ref/xamarinios10/_._", + "ref/xamarinmac20/_._", + "ref/xamarintvos10/_._", + "ref/xamarinwatchos10/_._", + "runtimes/aot/lib/netcore50/System.Reflection.Emit.dll", + "runtimes/aot/lib/netcore50/System.Reflection.Emit.xml", + "system.reflection.emit.4.7.0.nupkg.sha512", + "system.reflection.emit.nuspec", + "useSharedDesignerContext.txt", + "version.txt" + ] + } }, - "libraries": {}, "projectFileDependencyGroups": { - ".NETStandard,Version=v2.1": [] + ".NETStandard,Version=v2.1": [ + "Lib.Harmony >= 2.4.1" + ] }, "packageFolders": { "C:\\Users\\Lenovo\\.nuget\\packages\\": {}, @@ -56,6 +210,12 @@ "frameworks": { "netstandard2.1": { "targetAlias": "netstandard2.1", + "dependencies": { + "Lib.Harmony": { + "target": "Package", + "version": "[2.4.1, )" + } + }, "imports": [ "net461", "net462", diff --git a/UIFrame/obj/project.nuget.cache b/UIFrame/obj/project.nuget.cache index 46bb9aa..b0173c9 100644 --- a/UIFrame/obj/project.nuget.cache +++ b/UIFrame/obj/project.nuget.cache @@ -1,8 +1,12 @@ { "version": 2, - "dgSpecHash": "A4ZumVBkozg=", + "dgSpecHash": "b0JcKXS9rW0=", "success": true, "projectFilePath": "D:\\vs_project\\DuckovMods\\UIFrame\\UIFrame.csproj", - "expectedPackageFiles": [], + "expectedPackageFiles": [ + "C:\\Users\\Lenovo\\.nuget\\packages\\lib.harmony\\2.4.1\\lib.harmony.2.4.1.nupkg.sha512", + "C:\\Users\\Lenovo\\.nuget\\packages\\lib.harmony.ref\\2.4.1\\lib.harmony.ref.2.4.1.nupkg.sha512", + "C:\\Users\\Lenovo\\.nuget\\packages\\system.reflection.emit\\4.7.0\\system.reflection.emit.4.7.0.nupkg.sha512" + ], "logs": [] } \ No newline at end of file diff --git a/UIFrame/obj/project.packagespec.json b/UIFrame/obj/project.packagespec.json index 477c4e3..c67fbaf 100644 --- a/UIFrame/obj/project.packagespec.json +++ b/UIFrame/obj/project.packagespec.json @@ -1 +1 @@ -"restore":{"projectUniqueName":"D:\\vs_project\\DuckovMods\\UIFrame\\UIFrame.csproj","projectName":"UIFrame","projectPath":"D:\\vs_project\\DuckovMods\\UIFrame\\UIFrame.csproj","outputPath":"D:\\vs_project\\DuckovMods\\UIFrame\\obj\\","projectStyle":"PackageReference","fallbackFolders":["D:\\vsShare\\NuGetPackages"],"originalTargetFrameworks":["netstandard2.1"],"sources":{"C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\":{},"https://api.nuget.org/v3/index.json":{}},"frameworks":{"netstandard2.1":{"targetAlias":"netstandard2.1","projectReferences":{}}},"warningProperties":{"warnAsError":["NU1605"]},"restoreAuditProperties":{"enableAudit":"true","auditLevel":"low","auditMode":"direct"},"SdkAnalysisLevel":"9.0.300"}"frameworks":{"netstandard2.1":{"targetAlias":"netstandard2.1","imports":["net461","net462","net47","net471","net472","net48","net481"],"assetTargetFallback":true,"warn":true,"frameworkReferences":{"NETStandard.Library":{"privateAssets":"all"}},"runtimeIdentifierGraphPath":"C:\\Program Files\\dotnet\\sdk\\9.0.306\\RuntimeIdentifierGraph.json"}} \ No newline at end of file +"restore":{"projectUniqueName":"D:\\vs_project\\DuckovMods\\UIFrame\\UIFrame.csproj","projectName":"UIFrame","projectPath":"D:\\vs_project\\DuckovMods\\UIFrame\\UIFrame.csproj","outputPath":"D:\\vs_project\\DuckovMods\\UIFrame\\obj\\","projectStyle":"PackageReference","fallbackFolders":["D:\\vsShare\\NuGetPackages"],"originalTargetFrameworks":["netstandard2.1"],"sources":{"C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\":{},"https://api.nuget.org/v3/index.json":{}},"frameworks":{"netstandard2.1":{"targetAlias":"netstandard2.1","projectReferences":{}}},"warningProperties":{"warnAsError":["NU1605"]},"restoreAuditProperties":{"enableAudit":"true","auditLevel":"low","auditMode":"direct"},"SdkAnalysisLevel":"9.0.300"}"frameworks":{"netstandard2.1":{"targetAlias":"netstandard2.1","dependencies":{"Lib.Harmony":{"target":"Package","version":"[2.4.1, )"}},"imports":["net461","net462","net47","net471","net472","net48","net481"],"assetTargetFallback":true,"warn":true,"frameworkReferences":{"NETStandard.Library":{"privateAssets":"all"}},"runtimeIdentifierGraphPath":"C:\\Program Files\\dotnet\\sdk\\9.0.306\\RuntimeIdentifierGraph.json"}} \ No newline at end of file diff --git a/UIFrame/obj/rider.project.restore.info b/UIFrame/obj/rider.project.restore.info index cb867e2..ea4a121 100644 --- a/UIFrame/obj/rider.project.restore.info +++ b/UIFrame/obj/rider.project.restore.info @@ -1 +1 @@ -17620796287866771 \ No newline at end of file +17623343068138064 \ No newline at end of file