From 1e3cb8283a5c4c3fb82389be290b17dc6e8e83e6 Mon Sep 17 00:00:00 2001 From: OLC Date: Sun, 16 Nov 2025 17:20:58 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=8F=97?= =?UTF-8?q?=E4=BC=A4=E4=B8=8E=E6=90=9C=E7=B4=A2=E9=9F=B3=E6=95=88=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 5 ++ CHANGELOG_EN.md | 5 ++ DuckovCustomModel.Core/Data/SoundTags.cs | 20 ++++++++ DuckovCustomModel/Constant.cs | 14 +---- .../HarmonyPatches/ItemDisplayPatches.cs | 51 +++++++++++++++++++ .../MonoBehaviours/ModelHandler.cs | 16 +++++- README.md | 9 ++++ README_EN.md | 11 ++++ 8 files changed, 118 insertions(+), 13 deletions(-) create mode 100644 DuckovCustomModel/HarmonyPatches/ItemDisplayPatches.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index e946a18..7d6ead7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ [English](CHANGELOG_EN.md) | 中文 +## v1.8.1-dev + +- 新增音效标签 `trigger_on_hurt`,用于角色受伤时自动播放音效 +- 新增 `search_found_item_quality_xxx` 系列音效标签,可在搜索完成时根据物品品质触发不同音效(支持 none/white/green/blue/purple/orange/red/q7/q8) + ## v1.8.0-fix1 - 修复了因修改加载方式导致的多语言加载失败问题 diff --git a/CHANGELOG_EN.md b/CHANGELOG_EN.md index 452e907..0483169 100644 --- a/CHANGELOG_EN.md +++ b/CHANGELOG_EN.md @@ -2,6 +2,11 @@ English | [中文](CHANGELOG.md) +## v1.8.1-dev + +- Added sound tag `trigger_on_hurt` for automatically playing sounds when a character is hurt +- Added `search_found_item_quality_xxx` tag series so different sounds can play after finishing a search based on the revealed item quality (supports none/white/green/blue/purple/orange/red/q7/q8) + ## v1.8.0-fix1 - Fixed issue where multilingual loading failed due to changes in loading method diff --git a/DuckovCustomModel.Core/Data/SoundTags.cs b/DuckovCustomModel.Core/Data/SoundTags.cs index 55735e2..db3e5d6 100644 --- a/DuckovCustomModel.Core/Data/SoundTags.cs +++ b/DuckovCustomModel.Core/Data/SoundTags.cs @@ -8,7 +8,17 @@ public static class SoundTags public const string Surprise = "surprise"; public const string Death = "death"; public const string Idle = "idle"; + public const string TriggerOnHurt = "trigger_on_hurt"; public const string TriggerOnDeath = "trigger_on_death"; + public const string SearchFoundItemQualityNone = "search_found_item_quality_none"; + public const string SearchFoundItemQualityWhite = "search_found_item_quality_white"; + public const string SearchFoundItemQualityGreen = "search_found_item_quality_green"; + public const string SearchFoundItemQualityBlue = "search_found_item_quality_blue"; + public const string SearchFoundItemQualityPurple = "search_found_item_quality_purple"; + public const string SearchFoundItemQualityOrange = "search_found_item_quality_orange"; + public const string SearchFoundItemQualityRed = "search_found_item_quality_red"; + public const string SearchFoundItemQualityQ7 = "search_found_item_quality_q7"; + public const string SearchFoundItemQualityQ8 = "search_found_item_quality_q8"; public static IReadOnlyCollection ValidTags => [ @@ -16,7 +26,17 @@ public static class SoundTags Surprise, Death, Idle, + TriggerOnHurt, TriggerOnDeath, + SearchFoundItemQualityNone, + SearchFoundItemQualityWhite, + SearchFoundItemQualityGreen, + SearchFoundItemQualityBlue, + SearchFoundItemQualityPurple, + SearchFoundItemQualityOrange, + SearchFoundItemQualityRed, + SearchFoundItemQualityQ7, + SearchFoundItemQualityQ8, ]; } } diff --git a/DuckovCustomModel/Constant.cs b/DuckovCustomModel/Constant.cs index f057c97..473e993 100644 --- a/DuckovCustomModel/Constant.cs +++ b/DuckovCustomModel/Constant.cs @@ -1,20 +1,10 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; - -namespace DuckovCustomModel +namespace DuckovCustomModel { public static class Constant { public const string ModID = "DuckovCustomModel"; public const string ModName = "Duckov Custom Model"; - public const string ModVersion = "1.8.0-fix1"; + public const string ModVersion = "1.8.1-dev"; public const string HarmonyId = "com.ritsukage.DuckovCustomModel"; - - public static readonly JsonSerializerSettings JsonSettings = new() - { - TypeNameHandling = TypeNameHandling.Auto, - Formatting = Formatting.Indented, - Converters = [new StringEnumConverter()], - }; } } diff --git a/DuckovCustomModel/HarmonyPatches/ItemDisplayPatches.cs b/DuckovCustomModel/HarmonyPatches/ItemDisplayPatches.cs new file mode 100644 index 0000000..7157eb7 --- /dev/null +++ b/DuckovCustomModel/HarmonyPatches/ItemDisplayPatches.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using Duckov; +using Duckov.UI; +using DuckovCustomModel.Core.Data; +using DuckovCustomModel.MonoBehaviours; +using HarmonyLib; +using ItemStatsSystem; + +namespace DuckovCustomModel.HarmonyPatches +{ + [HarmonyPatch] + internal class ItemDisplayPatches + { + private static readonly IReadOnlyDictionary QualitySoundTags = + new Dictionary + { + { DisplayQuality.None, SoundTags.SearchFoundItemQualityNone }, + { DisplayQuality.White, SoundTags.SearchFoundItemQualityWhite }, + { DisplayQuality.Green, SoundTags.SearchFoundItemQualityGreen }, + { DisplayQuality.Blue, SoundTags.SearchFoundItemQualityBlue }, + { DisplayQuality.Purple, SoundTags.SearchFoundItemQualityPurple }, + { DisplayQuality.Orange, SoundTags.SearchFoundItemQualityOrange }, + { DisplayQuality.Red, SoundTags.SearchFoundItemQualityRed }, + { DisplayQuality.Q7, SoundTags.SearchFoundItemQualityQ7 }, + { DisplayQuality.Q8, SoundTags.SearchFoundItemQualityQ8 }, + }; + + [HarmonyPatch(typeof(ItemDisplay), nameof(ItemDisplay.OnTargetInspectionStateChanged))] + [HarmonyPostfix] + private static void ItemDisplay_OnTargetInspectionStateChanged_Postfix(Item item) + { + if (item == null) return; + if (item.Inspecting || !item.Inspected) return; + + var mainPlayer = CharacterMainControl.Main; + if (mainPlayer == null) return; + + var modelHandler = mainPlayer.GetComponent(); + if (modelHandler == null || !modelHandler.IsInitialized) return; + if (!modelHandler.IsModelAudioEnabled) return; + if (!modelHandler.HasAnySounds()) return; + + if (!QualitySoundTags.TryGetValue(item.DisplayQuality, out var soundTag)) return; + + var soundPath = modelHandler.GetRandomSoundByTag(soundTag); + if (string.IsNullOrEmpty(soundPath)) return; + + AudioManager.PostCustomSFX(soundPath); + } + } +} diff --git a/DuckovCustomModel/MonoBehaviours/ModelHandler.cs b/DuckovCustomModel/MonoBehaviours/ModelHandler.cs index 1dea612..52a640b 100644 --- a/DuckovCustomModel/MonoBehaviours/ModelHandler.cs +++ b/DuckovCustomModel/MonoBehaviours/ModelHandler.cs @@ -199,7 +199,11 @@ public void Initialize(CharacterMainControl characterMainControl, ModelTarget ta RecordOriginalHeadCollider(); RecordOriginalSoundMaker(); - if (CharacterMainControl.Health != null) CharacterMainControl.Health.OnDeadEvent.AddListener(OnDeath); + if (CharacterMainControl.Health != null) + { + CharacterMainControl.Health.OnHurtEvent.AddListener(OnHurt); + CharacterMainControl.Health.OnDeadEvent.AddListener(OnDeath); + } ModLogger.Log("ModelHandler initialized successfully."); IsInitialized = true; @@ -761,6 +765,16 @@ private void ForceUpdateHealthBar() healthBar.RefreshOffset(); } + private void OnHurt(DamageInfo damageInfo) + { + if (!IsModelAudioEnabled) return; + + var soundPath = GetRandomSoundByTag(SoundTags.TriggerOnHurt); + if (string.IsNullOrEmpty(soundPath)) return; + + AudioManager.PostCustomSFX(soundPath); + } + private void OnDeath(DamageInfo damageInfo) { if (!IsModelAudioEnabled) return; diff --git a/README.md b/README.md index 02fea55..0b789ac 100644 --- a/README.md +++ b/README.md @@ -612,7 +612,9 @@ Animator Controller 可以使用以下参数: - `"surprise"`:惊讶音效,用于 AI 惊讶状态 - `"death"`:死亡音效,用于 AI 死亡状态 - `"idle"`:待机音效,用于角色自动播放(可通过配置控制哪些角色类型允许自动播放) + - `"trigger_on_hurt"`:受伤触发音效,用于角色受到伤害时自动播放 - `"trigger_on_death"`:死亡触发音效,用于角色死亡时自动播放 + - `"search_found_item_quality_xxx"`:搜索完成时发现指定品质物品会触发音效,`xxx` 可为 `none`、`white`、`green`、`blue`、`purple`、`orange`、`red`、`q7`、`q8` - 可以同时包含多个标签,表示该音效可用于多个场景 - 未指定标签时,默认为 `["normal"]` @@ -633,6 +635,7 @@ Animator Controller 可以使用以下参数: - `"normal"`:AI 普通状态时触发 - `"surprise"`:AI 惊讶状态时触发 - `"death"`:AI 死亡状态时触发 +- `"trigger_on_hurt"`:角色受到伤害时自动播放(适用于所有角色类型) - `"idle"`:启用了自动播放的角色会在随机间隔时间自动播放待机音效 - `"trigger_on_death"`:角色死亡时自动播放(适用于所有角色类型) - 播放间隔可在 `IdleAudioConfig.json` 中配置 @@ -642,6 +645,12 @@ Animator Controller 可以使用以下参数: - 默认情况下,AI 角色和宠物允许自动播放,玩家角色不允许(可通过配置启用) - 如果指定标签的音效不存在,将使用原版事件(不会回退到其他标签) +#### 搜索发现触发 + +- 玩家完成物品搜索或检查后(UI 展示品质信息的那一刻)会触发对应品质的音效 +- 使用 `search_found_item_quality_xxx` 标签,`xxx` 与 `Tags` 描述相同:`none`、`white`、`green`、`blue`、`purple`、`orange`、`red`、`q7`、`q8` +- 若模型未配置对应品质的音效,则不会播放,保持与原版一致 + ### 音效文件要求 - 音效文件应放置在模型包文件夹内 diff --git a/README_EN.md b/README_EN.md index 628e000..9ac84f9 100644 --- a/README_EN.md +++ b/README_EN.md @@ -606,6 +606,9 @@ Sounds can be configured in `ModelInfo` within `bundleinfo.json`: - `"surprise"`: Surprise sound, used for AI surprise state - `"death"`: Death sound, used for AI death state - `"idle"`: Idle sound, used for automatic playback by characters (can be controlled through configuration to determine which character types are allowed to automatically play) + - `"trigger_on_hurt"`: Hurt trigger sound, automatically plays when the character takes damage + - `"trigger_on_death"`: Death trigger sound, automatically plays when the character dies + - `"search_found_item_quality_xxx"`: Plays when a searched item of the specified quality is revealed; `xxx` can be `none`, `white`, `green`, `blue`, `purple`, `orange`, `red`, `q7`, or `q8` - Can contain multiple tags, indicating the sound can be used in multiple scenarios - Defaults to `["normal"]` when no tags are specified @@ -626,7 +629,9 @@ Sounds can be configured in `ModelInfo` within `bundleinfo.json`: - `"normal"`: Triggered during AI normal state - `"surprise"`: Triggered during AI surprise state - `"death"`: Triggered during AI death state +- `"trigger_on_hurt"`: Automatically plays when the character takes damage (applies to all character types) - `"idle"`: Characters with automatic playback enabled will automatically play idle sounds at random intervals +- `"trigger_on_death"`: Automatically plays when the character dies (applies to all character types) - Play interval can be configured in `IdleAudioConfig.json` - Default interval is 30-45 seconds (random) - Will not play when the character is dead @@ -634,6 +639,12 @@ Sounds can be configured in `ModelInfo` within `bundleinfo.json`: - By default, AI characters and pets are allowed to automatically play, while player characters are not (can be enabled through configuration) - If a sound with the specified tag doesn't exist, the original game event will be used (no fallback to other tags) +#### Search Discovery Trigger + +- When the player finishes searching or inspecting an item and its quality is revealed, the corresponding sound tag is triggered +- Use `search_found_item_quality_xxx`, where `xxx` matches the same quality suffix listed in `Tags`: `none`, `white`, `green`, `blue`, `purple`, `orange`, `red`, `q7`, `q8` +- If no sound is configured for that quality, nothing plays and the vanilla behavior remains unchanged + ### Sound File Requirements - Sound files should be placed inside the model bundle folder From d65a26fed1eff3860257486c61d42584e29d9a32 Mon Sep 17 00:00:00 2001 From: OLC Date: Sun, 16 Nov 2025 18:04:13 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=89=A9?= =?UTF-8?q?=E5=93=81=E7=94=9F=E5=91=BD=E5=91=A8=E6=9C=9F=E8=B7=9F=E8=B8=AA?= =?UTF-8?q?=E7=A1=AE=E4=BF=9D=E5=93=81=E8=B4=A8=E9=9F=B3=E6=95=88=E5=9C=A8?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=E5=AE=8C=E6=88=90=E6=97=B6=E6=AD=A3=E7=A1=AE?= =?UTF-8?q?=E8=A7=A6=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HarmonyPatches/ItemDisplayPatches.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/DuckovCustomModel/HarmonyPatches/ItemDisplayPatches.cs b/DuckovCustomModel/HarmonyPatches/ItemDisplayPatches.cs index 7157eb7..5d37f20 100644 --- a/DuckovCustomModel/HarmonyPatches/ItemDisplayPatches.cs +++ b/DuckovCustomModel/HarmonyPatches/ItemDisplayPatches.cs @@ -25,12 +25,30 @@ internal class ItemDisplayPatches { DisplayQuality.Q8, SoundTags.SearchFoundItemQualityQ8 }, }; + private static readonly HashSet RecordedItems = []; + + [HarmonyPatch(typeof(ItemDisplay), nameof(ItemDisplay.Setup))] + [HarmonyPostfix] + // ReSharper disable once InconsistentNaming + private static void ItemDisplay_Setup_Postfix(ItemDisplay __instance, Item target) + { + if (target == null) return; + if (RecordedItems.Contains(target)) return; + if (!target.NeedInspection) return; + + RecordedItems.Add(target); + target.onDestroy += OnItemDestroyed; + } + [HarmonyPatch(typeof(ItemDisplay), nameof(ItemDisplay.OnTargetInspectionStateChanged))] [HarmonyPostfix] private static void ItemDisplay_OnTargetInspectionStateChanged_Postfix(Item item) { if (item == null) return; if (item.Inspecting || !item.Inspected) return; + if (!RecordedItems.Contains(item)) return; + + RecordedItems.Remove(item); var mainPlayer = CharacterMainControl.Main; if (mainPlayer == null) return; @@ -47,5 +65,13 @@ private static void ItemDisplay_OnTargetInspectionStateChanged_Postfix(Item item AudioManager.PostCustomSFX(soundPath); } + + private static void OnItemDestroyed(Item item) + { + if (item == null) return; + if (!RecordedItems.Contains(item)) return; + + RecordedItems.Remove(item); + } } } From ec699668bb364353818a44ca2522c03ae6b24337 Mon Sep 17 00:00:00 2001 From: OLC Date: Sun, 16 Nov 2025 18:06:02 +0800 Subject: [PATCH 3/4] =?UTF-8?q?chore:=20=E5=8F=91=E5=B8=83=20v1.8.1=20?= =?UTF-8?q?=E6=AD=A3=E5=BC=8F=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 +- CHANGELOG_EN.md | 2 +- DuckovCustomModel/Constant.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d6ead7..50d75af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ [English](CHANGELOG_EN.md) | 中文 -## v1.8.1-dev +## v1.8.1 - 新增音效标签 `trigger_on_hurt`,用于角色受伤时自动播放音效 - 新增 `search_found_item_quality_xxx` 系列音效标签,可在搜索完成时根据物品品质触发不同音效(支持 none/white/green/blue/purple/orange/red/q7/q8) diff --git a/CHANGELOG_EN.md b/CHANGELOG_EN.md index 0483169..30f5304 100644 --- a/CHANGELOG_EN.md +++ b/CHANGELOG_EN.md @@ -2,7 +2,7 @@ English | [中文](CHANGELOG.md) -## v1.8.1-dev +## v1.8.1 - Added sound tag `trigger_on_hurt` for automatically playing sounds when a character is hurt - Added `search_found_item_quality_xxx` tag series so different sounds can play after finishing a search based on the revealed item quality (supports none/white/green/blue/purple/orange/red/q7/q8) diff --git a/DuckovCustomModel/Constant.cs b/DuckovCustomModel/Constant.cs index 473e993..2bd7785 100644 --- a/DuckovCustomModel/Constant.cs +++ b/DuckovCustomModel/Constant.cs @@ -4,7 +4,7 @@ public static class Constant { public const string ModID = "DuckovCustomModel"; public const string ModName = "Duckov Custom Model"; - public const string ModVersion = "1.8.1-dev"; + public const string ModVersion = "1.8.1"; public const string HarmonyId = "com.ritsukage.DuckovCustomModel"; } } From f43fbb23c9c836344435550c087872555ea5234a Mon Sep 17 00:00:00 2001 From: OLC Date: Sun, 16 Nov 2025 18:13:50 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=E6=94=B9=E8=BF=9B=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E8=AE=A2=E9=98=85=E6=B8=85=E7=90=86=E9=80=BB=E8=BE=91=E9=98=B2?= =?UTF-8?q?=E6=AD=A2=E5=86=85=E5=AD=98=E6=B3=84=E6=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + CHANGELOG_EN.md | 1 + DuckovCustomModel/HarmonyPatches/ItemDisplayPatches.cs | 1 + DuckovCustomModel/MonoBehaviours/ModelHandler.cs | 8 ++++++++ 4 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50d75af..69c8f46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - 新增音效标签 `trigger_on_hurt`,用于角色受伤时自动播放音效 - 新增 `search_found_item_quality_xxx` 系列音效标签,可在搜索完成时根据物品品质触发不同音效(支持 none/white/green/blue/purple/orange/red/q7/q8) +- 改进了事件订阅的清理逻辑,防止内存泄漏 ## v1.8.0-fix1 diff --git a/CHANGELOG_EN.md b/CHANGELOG_EN.md index 30f5304..5740998 100644 --- a/CHANGELOG_EN.md +++ b/CHANGELOG_EN.md @@ -6,6 +6,7 @@ English | [中文](CHANGELOG.md) - Added sound tag `trigger_on_hurt` for automatically playing sounds when a character is hurt - Added `search_found_item_quality_xxx` tag series so different sounds can play after finishing a search based on the revealed item quality (supports none/white/green/blue/purple/orange/red/q7/q8) +- Improved event subscription cleanup logic to prevent memory leaks ## v1.8.0-fix1 diff --git a/DuckovCustomModel/HarmonyPatches/ItemDisplayPatches.cs b/DuckovCustomModel/HarmonyPatches/ItemDisplayPatches.cs index 5d37f20..febda20 100644 --- a/DuckovCustomModel/HarmonyPatches/ItemDisplayPatches.cs +++ b/DuckovCustomModel/HarmonyPatches/ItemDisplayPatches.cs @@ -48,6 +48,7 @@ private static void ItemDisplay_OnTargetInspectionStateChanged_Postfix(Item item if (item.Inspecting || !item.Inspected) return; if (!RecordedItems.Contains(item)) return; + item.onDestroy -= OnItemDestroyed; RecordedItems.Remove(item); var mainPlayer = CharacterMainControl.Main; diff --git a/DuckovCustomModel/MonoBehaviours/ModelHandler.cs b/DuckovCustomModel/MonoBehaviours/ModelHandler.cs index 52a640b..2160ad5 100644 --- a/DuckovCustomModel/MonoBehaviours/ModelHandler.cs +++ b/DuckovCustomModel/MonoBehaviours/ModelHandler.cs @@ -156,6 +156,14 @@ private void LateUpdate() } } + private void OnDestroy() + { + if (CharacterMainControl == null) return; + if (CharacterMainControl.Health == null) return; + CharacterMainControl.Health.OnHurtEvent.RemoveListener(OnHurt); + CharacterMainControl.Health.OnDeadEvent.RemoveListener(OnDeath); + } + public void Initialize(CharacterMainControl characterMainControl, ModelTarget target = ModelTarget.Character) { if (IsInitialized) return;