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